aimux-cli 0.1.19 → 0.1.20

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.
Files changed (86) hide show
  1. package/README.md +13 -4
  2. package/bin/aimux +4 -0
  3. package/bin/aimux-dev +2 -6
  4. package/dist/agent-output-parser-audit.d.ts +23 -0
  5. package/dist/agent-output-parser-audit.js +187 -0
  6. package/dist/agent-output-parser-contract.d.ts +9 -0
  7. package/dist/agent-output-parser-contract.js +33 -0
  8. package/dist/agent-output-parser-fixtures.d.ts +15 -0
  9. package/dist/agent-output-parser-fixtures.js +593 -0
  10. package/dist/agent-output-parser-harness.d.ts +21 -0
  11. package/dist/agent-output-parser-harness.js +43 -0
  12. package/dist/agent-output-parser-test-utils.d.ts +1 -0
  13. package/dist/agent-output-parser-test-utils.js +7 -0
  14. package/dist/agent-output-parser.js +215 -35
  15. package/dist/atomic-write.d.ts +15 -0
  16. package/dist/atomic-write.js +69 -4
  17. package/dist/attachment-store.d.ts +7 -0
  18. package/dist/attachment-store.js +64 -5
  19. package/dist/backend-session-discovery.d.ts +17 -0
  20. package/dist/backend-session-discovery.js +57 -0
  21. package/dist/config.js +9 -4
  22. package/dist/connection-targets.js +20 -1
  23. package/dist/context/context-bridge.js +4 -1
  24. package/dist/credentials.js +3 -6
  25. package/dist/daemon.d.ts +1 -0
  26. package/dist/daemon.js +16 -0
  27. package/dist/dashboard/index.d.ts +1 -0
  28. package/dist/dashboard/index.js +1 -0
  29. package/dist/dashboard/targets.js +14 -2
  30. package/dist/dashboard/ui-state-store.js +4 -3
  31. package/dist/last-used.js +3 -2
  32. package/dist/launcher-env.d.ts +4 -0
  33. package/dist/launcher-env.js +70 -0
  34. package/dist/main.js +16 -1
  35. package/dist/metadata-server.d.ts +13 -2
  36. package/dist/metadata-server.js +60 -4
  37. package/dist/metadata-store.js +4 -3
  38. package/dist/mobile-push-bridge.d.ts +8 -0
  39. package/dist/mobile-push-bridge.js +22 -0
  40. package/dist/mobile-push-throttle.d.ts +23 -0
  41. package/dist/mobile-push-throttle.js +53 -0
  42. package/dist/multiplexer/dashboard-model.js +3 -2
  43. package/dist/multiplexer/dashboard-ops.d.ts +3 -2
  44. package/dist/multiplexer/dashboard-ops.js +2 -2
  45. package/dist/multiplexer/dashboard-tail-methods.d.ts +3 -2
  46. package/dist/multiplexer/dashboard-tail-methods.js +2 -2
  47. package/dist/multiplexer/dashboard-view-methods.js +2 -0
  48. package/dist/multiplexer/index.d.ts +1 -1
  49. package/dist/multiplexer/index.js +4 -4
  50. package/dist/multiplexer/persistence-methods.js +2 -1
  51. package/dist/multiplexer/runtime-lifecycle-methods.js +6 -2
  52. package/dist/multiplexer/runtime-state.js +13 -1
  53. package/dist/multiplexer/service-state-snapshot.js +4 -2
  54. package/dist/multiplexer/services.js +5 -4
  55. package/dist/multiplexer/session-launch.d.ts +1 -1
  56. package/dist/multiplexer/session-launch.js +18 -6
  57. package/dist/multiplexer/session-runtime-core.js +9 -2
  58. package/dist/multiplexer/tool-picker.d.ts +2 -1
  59. package/dist/multiplexer/tool-picker.js +29 -21
  60. package/dist/notify.d.ts +1 -1
  61. package/dist/notify.js +8 -5
  62. package/dist/paths.js +50 -4
  63. package/dist/project-takeover.d.ts +1 -0
  64. package/dist/project-takeover.js +117 -0
  65. package/dist/relay-client.d.ts +10 -0
  66. package/dist/relay-client.js +5 -0
  67. package/dist/runtime-core/backend-id-reconcile.d.ts +13 -0
  68. package/dist/runtime-core/backend-id-reconcile.js +23 -0
  69. package/dist/runtime-core/exchange-store.js +3 -8
  70. package/dist/runtime-core/topology-store.js +3 -8
  71. package/dist/runtime-owner.d.ts +3 -0
  72. package/dist/runtime-owner.js +10 -0
  73. package/dist/shell-args.d.ts +13 -0
  74. package/dist/shell-args.js +25 -0
  75. package/dist/shell-hooks.d.ts +1 -0
  76. package/dist/shell-hooks.js +1 -0
  77. package/dist/team.js +4 -3
  78. package/dist/tmux/runtime-manager.js +2 -0
  79. package/dist/tui/screens/dashboard-renderers.js +6 -6
  80. package/dist/vitest.setup.d.ts +1 -0
  81. package/dist/vitest.setup.js +9 -0
  82. package/dist-ui/_expo/static/css/web-8782287775683e5a944b821b854d0f60.css +1 -0
  83. package/dist-ui/_expo/static/js/web/{entry-477c745b2adc79367a4380ecf07d9ff6.js → entry-90d00d223eefabe5cc21e4329b274fa5.js} +260 -252
  84. package/dist-ui/index.html +2 -2
  85. package/package.json +3 -1
  86. package/dist-ui/_expo/static/css/web-30453ede1678c16acb08b97e83e8646d.css +0 -1
package/dist/notify.js CHANGED
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
3
3
  import { loadConfig } from "./config.js";
4
4
  import { debug } from "./debug.js";
5
5
  import { shouldSuppressNotification } from "./notification-context.js";
6
+ import { forwardAlertToMobilePush } from "./mobile-push-bridge.js";
6
7
  let cachedConfig = null;
7
8
  function getNotifyConfig() {
8
9
  if (!cachedConfig) {
@@ -68,9 +69,9 @@ export function notifyComplete(sessionId) {
68
69
  export function notifyAlert(event) {
69
70
  const config = getNotifyConfig();
70
71
  if (!config.enabled)
71
- return;
72
+ return false;
72
73
  if (shouldSuppressNotification(event))
73
- return;
74
+ return false;
74
75
  if ((event.kind === "notification" ||
75
76
  event.kind === "needs_input" ||
76
77
  event.kind === "message_waiting" ||
@@ -78,13 +79,15 @@ export function notifyAlert(event) {
78
79
  event.kind === "task_assigned" ||
79
80
  event.kind === "review_waiting") &&
80
81
  !config.onPrompt) {
81
- return;
82
+ return false;
82
83
  }
83
84
  if (event.kind === "task_done" && !config.onComplete)
84
- return;
85
+ return false;
85
86
  if ((event.kind === "task_failed" || event.kind === "blocked") && !config.onError)
86
- return;
87
+ return false;
87
88
  send(event.title || "aimux", event.message || event.sessionId || event.kind);
89
+ forwardAlertToMobilePush(event);
90
+ return true;
88
91
  }
89
92
  export function notifyRemoteClientConnected(input) {
90
93
  const title = typeof input.title === "string" && input.title.trim().length > 0 ? input.title : "aimux remote access";
package/dist/paths.js CHANGED
@@ -12,9 +12,10 @@
12
12
  */
13
13
  import { createHash } from "node:crypto";
14
14
  import { execSync } from "node:child_process";
15
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from "node:fs";
16
16
  import { join, basename, resolve, dirname, sep } from "node:path";
17
- import { homedir } from "node:os";
17
+ import { homedir, tmpdir } from "node:os";
18
+ import { quarantineCorruptFile, writeJsonAtomic } from "./atomic-write.js";
18
19
  // ── Cached state (populated by initPaths) ──────────────────────────
19
20
  let _repoRoot = null;
20
21
  let _projectId = null;
@@ -264,25 +265,69 @@ export function getGlobalTeamPath() {
264
265
  export function getProjectsRegistryPath() {
265
266
  return join(getGlobalAimuxDir(), "projects.json");
266
267
  }
268
+ const MAX_PROJECT_REGISTRY_ENTRIES = 500;
269
+ const OS_TMP_DIR = tmpdir();
270
+ const TEMP_DIR_CANDIDATES = [OS_TMP_DIR, "/tmp", "/private/tmp", "/var/tmp"];
271
+ const TEMP_DIRS = (() => {
272
+ const dirs = new Set();
273
+ for (const dir of TEMP_DIR_CANDIDATES) {
274
+ const resolved = resolve(dir);
275
+ dirs.add(resolved);
276
+ try {
277
+ dirs.add(realpathSync(resolved));
278
+ }
279
+ catch { }
280
+ }
281
+ return [...dirs];
282
+ })();
283
+ function isPathInsideDir(path, dir) {
284
+ return path === dir || path.startsWith(`${dir}${sep}`);
285
+ }
286
+ function isEphemeralTempProjectRoot(repoRoot) {
287
+ const resolved = resolve(repoRoot);
288
+ return basename(resolved).startsWith("aimux-") && TEMP_DIRS.some((dir) => isPathInsideDir(resolved, dir));
289
+ }
290
+ function assertRegistryWithinCap(projects) {
291
+ if (projects.length <= MAX_PROJECT_REGISTRY_ENTRIES)
292
+ return;
293
+ throw new Error(`aimux project registry has ${projects.length} entries; cap is ${MAX_PROJECT_REGISTRY_ENTRIES}. ` +
294
+ `Refusing to continue because the registry is likely polluted. Remove stale entries from ${getProjectsRegistryPath()}.`);
295
+ }
296
+ function normalizeRegistry(registry) {
297
+ const projectsById = new Map();
298
+ for (const project of registry.projects) {
299
+ if (isEphemeralTempProjectRoot(project.repoRoot))
300
+ continue;
301
+ projectsById.set(project.id, project);
302
+ }
303
+ const projects = [...projectsById.values()];
304
+ assertRegistryWithinCap(projects);
305
+ return { version: 1, projects };
306
+ }
267
307
  function loadRegistry() {
268
308
  const path = getProjectsRegistryPath();
269
309
  if (!existsSync(path))
270
310
  return { version: 1, projects: [] };
311
+ let registry;
271
312
  try {
272
- return JSON.parse(readFileSync(path, "utf-8"));
313
+ registry = JSON.parse(readFileSync(path, "utf-8"));
273
314
  }
274
315
  catch {
316
+ quarantineCorruptFile(path);
275
317
  return { version: 1, projects: [] };
276
318
  }
319
+ return normalizeRegistry(registry);
277
320
  }
278
321
  function saveRegistry(registry) {
279
322
  const dir = getGlobalAimuxDir();
280
323
  if (!existsSync(dir))
281
324
  mkdirSync(dir, { recursive: true });
282
- writeFileSync(getProjectsRegistryPath(), JSON.stringify(registry, null, 2) + "\n");
325
+ writeJsonAtomic(getProjectsRegistryPath(), registry);
283
326
  }
284
327
  function registerProject() {
285
328
  assertInitialized();
329
+ if (isEphemeralTempProjectRoot(_repoRoot))
330
+ return;
286
331
  const registry = loadRegistry();
287
332
  const idx = registry.projects.findIndex((p) => p.id === _projectId);
288
333
  const entry = {
@@ -297,6 +342,7 @@ function registerProject() {
297
342
  else {
298
343
  registry.projects.push(entry);
299
344
  }
345
+ assertRegistryWithinCap(registry.projects);
300
346
  saveRegistry(registry);
301
347
  }
302
348
  export function listProjects() {
@@ -0,0 +1 @@
1
+ export declare function takeOverProjectFromOtherOwners(projectRoot: string): Promise<void>;
@@ -0,0 +1,117 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { getGlobalAimuxDir, getProjectIdFor } from "./paths.js";
6
+ import { writeJsonAtomic } from "./atomic-write.js";
7
+ import { requestJson } from "./http-client.js";
8
+ import { log } from "./debug.js";
9
+ function isPidAlive(pid) {
10
+ try {
11
+ process.kill(pid, 0);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ function knownOwners() {
19
+ return [
20
+ { home: join(homedir(), ".aimux"), port: 43190 },
21
+ { home: join(homedir(), ".aimux-dev"), port: 43191 },
22
+ ];
23
+ }
24
+ function readJson(path) {
25
+ if (!existsSync(path))
26
+ return null;
27
+ try {
28
+ return JSON.parse(readFileSync(path, "utf-8"));
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ function writeJson(path, value) {
35
+ writeJsonAtomic(path, value);
36
+ }
37
+ function isAimuxProjectServiceProcess(pid) {
38
+ try {
39
+ const args = execFileSync("ps", ["-o", "args=", "-p", String(pid)], {
40
+ encoding: "utf8",
41
+ stdio: ["ignore", "pipe", "ignore"],
42
+ });
43
+ return args.includes("__project-service-internal");
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function requestOtherOwnerStop(owner, projectRoot) {
50
+ const info = readJson(join(owner.home, "daemon", "daemon.json"));
51
+ const port = info?.port ?? owner.port;
52
+ if (!info?.pid || !isPidAlive(info.pid))
53
+ return false;
54
+ try {
55
+ const { status, json } = await requestJson(`http://127.0.0.1:${port}/projects/stop`, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json" },
58
+ body: { projectRoot },
59
+ timeoutMs: 1500,
60
+ });
61
+ return status >= 200 && status < 300 && json?.ok !== false;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ function cleanOtherOwnerProjectState(owner, projectId) {
68
+ const statePath = join(owner.home, "daemon", "state.json");
69
+ const state = readJson(statePath);
70
+ const entry = state?.projects?.[projectId];
71
+ if (entry?.pid && isPidAlive(entry.pid)) {
72
+ if (isAimuxProjectServiceProcess(entry.pid)) {
73
+ try {
74
+ process.kill(entry.pid, "SIGTERM");
75
+ }
76
+ catch { }
77
+ }
78
+ else {
79
+ log.warn("skipping stale takeover pid with unverified identity", "daemon", {
80
+ ownerHome: owner.home,
81
+ projectId,
82
+ projectRoot: entry.projectRoot,
83
+ pid: entry.pid,
84
+ });
85
+ }
86
+ }
87
+ if (state?.projects?.[projectId]) {
88
+ delete state.projects[projectId];
89
+ writeJson(statePath, state);
90
+ }
91
+ const projectStateDir = join(owner.home, "projects", projectId);
92
+ for (const file of ["metadata-api.json", "metadata-api.txt", "host.json"]) {
93
+ try {
94
+ rmSync(join(projectStateDir, file), { force: true });
95
+ }
96
+ catch { }
97
+ }
98
+ }
99
+ export async function takeOverProjectFromOtherOwners(projectRoot) {
100
+ const currentHome = resolve(getGlobalAimuxDir());
101
+ const projectId = getProjectIdFor(projectRoot);
102
+ for (const owner of knownOwners()) {
103
+ if (resolve(owner.home) === currentHome)
104
+ continue;
105
+ try {
106
+ await requestOtherOwnerStop(owner, projectRoot);
107
+ cleanOtherOwnerProjectState(owner, projectId);
108
+ }
109
+ catch (error) {
110
+ log.warn("project takeover cleanup failed for alternate owner", "daemon", {
111
+ ownerHome: owner.home,
112
+ projectRoot,
113
+ error: error instanceof Error ? error.message : String(error),
114
+ });
115
+ }
116
+ }
117
+ }
@@ -1,4 +1,13 @@
1
1
  import type { AimuxDaemon } from "./daemon.js";
2
+ export interface RelayNotificationPush {
3
+ title: string;
4
+ body: string;
5
+ kind?: string;
6
+ sessionId?: string;
7
+ projectId?: string;
8
+ projectRoot?: string;
9
+ dedupeKey?: string;
10
+ }
2
11
  export type RelayConnectionStatus = "connected" | "connecting" | "reconnecting" | "disconnected" | "auth_failed";
3
12
  export interface RelayStatusSnapshot {
4
13
  status: RelayConnectionStatus;
@@ -22,6 +31,7 @@ export declare class RelayClient {
22
31
  constructor(relayUrl: string, token: string, daemon: AimuxDaemon);
23
32
  getStatus(): RelayStatusSnapshot;
24
33
  connect(): void;
34
+ pushNotification(notification: RelayNotificationPush): void;
25
35
  disconnect(): void;
26
36
  private handleMessage;
27
37
  private sendRaw;
@@ -100,6 +100,11 @@ export class RelayClient {
100
100
  catch { }
101
101
  });
102
102
  }
103
+ pushNotification(notification) {
104
+ if (!notification.title)
105
+ return;
106
+ this.sendRaw(JSON.stringify({ type: "notification_push", notification }));
107
+ }
103
108
  disconnect() {
104
109
  this.stopped = true;
105
110
  this.status = "disconnected";
@@ -0,0 +1,13 @@
1
+ export interface BackendIdReconcileResult {
2
+ reconciled: Array<{
3
+ id: string;
4
+ backendSessionId: string;
5
+ }>;
6
+ }
7
+ /**
8
+ * Backfill missing backend session ids for offline sessions from each tool's
9
+ * on-disk session store, so a crash that lost the id before capture does not
10
+ * leave the agent unresumable. Idempotent: sessions that already have an id,
11
+ * or whose id cannot be found on disk, are left untouched.
12
+ */
13
+ export declare function reconcileOfflineBackendSessionIds(projectRoot?: string): BackendIdReconcileResult;
@@ -0,0 +1,23 @@
1
+ import { discoverBackendSessionId } from "../backend-session-discovery.js";
2
+ import { getRepoRoot } from "../paths.js";
3
+ import { listTopologySessionStates, upsertTopologySession } from "./topology-sessions.js";
4
+ /**
5
+ * Backfill missing backend session ids for offline sessions from each tool's
6
+ * on-disk session store, so a crash that lost the id before capture does not
7
+ * leave the agent unresumable. Idempotent: sessions that already have an id,
8
+ * or whose id cannot be found on disk, are left untouched.
9
+ */
10
+ export function reconcileOfflineBackendSessionIds(projectRoot = getRepoRoot()) {
11
+ const reconciled = [];
12
+ for (const session of listTopologySessionStates({ statuses: ["offline"] })) {
13
+ if (session.backendSessionId)
14
+ continue;
15
+ const cwd = session.worktreePath ?? projectRoot;
16
+ const discovered = discoverBackendSessionId(session.toolConfigKey, cwd);
17
+ if (!discovered)
18
+ continue;
19
+ upsertTopologySession({ ...session, backendSessionId: discovered }, "offline", { projectRoot });
20
+ reconciled.push({ id: session.id, backendSessionId: discovered });
21
+ }
22
+ return { reconciled };
23
+ }
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
+ import { atomicWrite } from "../atomic-write.js";
4
5
  import { getRuntimeExchangePath } from "../paths.js";
5
6
  export const RUNTIME_EXCHANGE_VERSION = 1;
6
7
  const UPDATE_LOCK_TIMEOUT_MS = 5_000;
@@ -376,14 +377,8 @@ export class RuntimeExchangeStore {
376
377
  return coerceRuntimeExchange(parsed);
377
378
  }
378
379
  write(exchange) {
379
- mkdirSync(dirname(this.path), { recursive: true });
380
380
  const normalized = coerceRuntimeExchange(exchange);
381
- const tmpPath = `${this.path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
382
- writeFileSync(tmpPath, stringify(normalized, {
383
- lineWidth: 120,
384
- sortMapEntries: false,
385
- }));
386
- renameSync(tmpPath, this.path);
381
+ atomicWrite(this.path, stringify(normalized, { lineWidth: 120, sortMapEntries: false }));
387
382
  return normalized;
388
383
  }
389
384
  acquireUpdateLock() {
@@ -1,6 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { parse, stringify } from "yaml";
4
+ import { atomicWrite } from "../atomic-write.js";
4
5
  import { getRuntimeTopologyPath } from "../paths.js";
5
6
  export const RUNTIME_TOPOLOGY_VERSION = 1;
6
7
  const UPDATE_LOCK_TIMEOUT_MS = 5_000;
@@ -370,14 +371,8 @@ export class RuntimeTopologyStore {
370
371
  return coerceRuntimeTopology(parsed);
371
372
  }
372
373
  write(topology) {
373
- mkdirSync(dirname(this.path), { recursive: true });
374
374
  const normalized = coerceRuntimeTopology(topology);
375
- const tmpPath = `${this.path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
376
- writeFileSync(tmpPath, stringify(normalized, {
377
- lineWidth: 120,
378
- sortMapEntries: false,
379
- }));
380
- renameSync(tmpPath, this.path);
375
+ atomicWrite(this.path, stringify(normalized, { lineWidth: 120, sortMapEntries: false }));
381
376
  return normalized;
382
377
  }
383
378
  acquireUpdateLock() {
@@ -0,0 +1,3 @@
1
+ export declare const TMUX_RUNTIME_OWNER_OPTION = "@aimux-runtime-owner";
2
+ export declare const TMUX_DASHBOARD_OWNER_OPTION = "@aimux-dashboard-owner";
3
+ export declare function getRuntimeOwnerId(): string;
@@ -0,0 +1,10 @@
1
+ import { getGlobalAimuxDir } from "./paths.js";
2
+ const DEFAULT_DAEMON_PORT = "43190";
3
+ export const TMUX_RUNTIME_OWNER_OPTION = "@aimux-runtime-owner";
4
+ export const TMUX_DASHBOARD_OWNER_OPTION = "@aimux-dashboard-owner";
5
+ export function getRuntimeOwnerId() {
6
+ return JSON.stringify({
7
+ home: getGlobalAimuxDir(),
8
+ port: process.env.AIMUX_DAEMON_PORT?.trim() || DEFAULT_DAEMON_PORT,
9
+ });
10
+ }
@@ -1 +1,14 @@
1
+ export interface LaunchOverride {
2
+ /** Binary to exec. May differ from the tool's configured command. */
3
+ command: string;
4
+ /** Full argument list (replaces the tool's default args, not appended). */
5
+ args: string[];
6
+ /** Extra env vars merged into the managed launch environment. */
7
+ env?: Record<string, string>;
8
+ }
9
+ /**
10
+ * Parse a full command line where leading NAME=VALUE tokens are environment
11
+ * assignments, the next token is the command, and the rest are its args.
12
+ */
13
+ export declare function parseLaunchCommandLine(input: string): LaunchOverride;
1
14
  export declare function parseShellArgs(input: string): string[];
@@ -1,3 +1,28 @@
1
+ const ENV_ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
2
+ /**
3
+ * Parse a full command line where leading NAME=VALUE tokens are environment
4
+ * assignments, the next token is the command, and the rest are its args.
5
+ */
6
+ export function parseLaunchCommandLine(input) {
7
+ const tokens = parseShellArgs(input);
8
+ const env = {};
9
+ let i = 0;
10
+ for (; i < tokens.length; i++) {
11
+ const match = ENV_ASSIGNMENT.exec(tokens[i]);
12
+ if (!match)
13
+ break;
14
+ env[match[1]] = match[2];
15
+ }
16
+ const command = tokens[i];
17
+ if (!command) {
18
+ throw new Error("no command to launch");
19
+ }
20
+ return {
21
+ command,
22
+ args: tokens.slice(i + 1),
23
+ env: Object.keys(env).length ? env : undefined,
24
+ };
25
+ }
1
26
  export function parseShellArgs(input) {
2
27
  const args = [];
3
28
  let current = "";
@@ -14,6 +14,7 @@ export declare function wrapCommandWithShellIntegration(opts: {
14
14
  args: string[];
15
15
  shellPath?: string;
16
16
  env?: NodeJS.ProcessEnv;
17
+ extraEnv?: Record<string, string>;
17
18
  }): {
18
19
  command: string;
19
20
  args: string[];
@@ -163,6 +163,7 @@ export function prepareShellIntegration(projectRoot, shellPath = process.env.SHE
163
163
  export function wrapCommandWithShellIntegration(opts) {
164
164
  const prepared = prepareShellIntegration(opts.projectRoot, opts.shellPath);
165
165
  const envArgs = [
166
+ ...Object.entries(opts.extraEnv ?? {}).map(([key, value]) => `${key}=${value}`),
166
167
  `AIMUX_SESSION_ID=${opts.sessionId}`,
167
168
  `AIMUX_TOOL=${opts.tool}`,
168
169
  `AIMUX_METADATA_ENDPOINT_FILE=${join(getProjectStateDirFor(opts.projectRoot), "metadata-api.txt")}`,
package/dist/team.js CHANGED
@@ -1,5 +1,6 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
1
+ import { existsSync, readFileSync, mkdirSync } from "node:fs";
2
2
  import { getLocalAimuxDir, getProjectTeamPath, getGlobalTeamPath, getGlobalAimuxDir } from "./paths.js";
3
+ import { writeJsonAtomic } from "./atomic-write.js";
3
4
  export function isTeammateSession(session) {
4
5
  return Boolean(session?.team?.parentSessionId);
5
6
  }
@@ -91,7 +92,7 @@ export function saveTeamConfig(config) {
91
92
  if (!existsSync(dir)) {
92
93
  mkdirSync(dir, { recursive: true });
93
94
  }
94
- writeFileSync(getProjectTeamPath(), JSON.stringify(config, null, 2) + "\n");
95
+ writeJsonAtomic(getProjectTeamPath(), config);
95
96
  }
96
97
  /**
97
98
  * Save team config at the global level (~/.aimux/team.json).
@@ -101,7 +102,7 @@ export function saveGlobalTeamConfig(config) {
101
102
  if (!existsSync(dir)) {
102
103
  mkdirSync(dir, { recursive: true });
103
104
  }
104
- writeFileSync(getGlobalTeamPath(), JSON.stringify(config, null, 2) + "\n");
105
+ writeJsonAtomic(getGlobalTeamPath(), config);
105
106
  }
106
107
  /**
107
108
  * Build a role-specific preamble string for injection into an agent's system prompt.
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import { loadConfig } from "../config.js";
8
8
  import { debug, log } from "../debug.js";
9
9
  import { getProjectStateDirFor } from "../paths.js";
10
+ import { getRuntimeOwnerId, TMUX_RUNTIME_OWNER_OPTION } from "../runtime-owner.js";
10
11
  export function isDashboardWindowName(name) {
11
12
  return name === "dashboard" || name.startsWith("dashboard-");
12
13
  }
@@ -677,6 +678,7 @@ export class TmuxRuntimeManager {
677
678
  const statuslineCommand = this.getStatuslineCommandSpec();
678
679
  const projectStateDir = getProjectStateDirFor(projectRoot);
679
680
  this.exec(["set-option", "-t", sessionName, "@aimux-project-root", projectRoot]);
681
+ this.exec(["set-option", "-t", sessionName, TMUX_RUNTIME_OWNER_OPTION, getRuntimeOwnerId()]);
680
682
  this.exec(["set-option", "-t", sessionName, "prefix", MANAGED_TMUX_SESSION_OPTIONS.prefix]);
681
683
  this.exec(["set-option", "-t", sessionName, "prefix2", MANAGED_TMUX_SESSION_OPTIONS.prefix2]);
682
684
  this.exec(["set-option", "-t", sessionName, "mouse", MANAGED_TMUX_SESSION_OPTIONS.mouse]);
@@ -422,12 +422,12 @@ export function renderDashboardFrame(state, cols, rows) {
422
422
  lines.push("");
423
423
  return lines.slice(0, height);
424
424
  };
425
- const header = [
426
- "",
427
- centerInBlock(`\x1b[1maimux\x1b[0m agent multiplexer${state.runtimeLabel ? ` \x1b[32m● ${state.runtimeLabel}\x1b[0m` : ""}`),
428
- "─".repeat(Math.max(0, cols)),
429
- "",
430
- ];
425
+ const devBadge = state.isDevRuntime ? "\x1b[1;30;43m DEV \x1b[0m " : "";
426
+ const title = `${devBadge}\x1b[1maimux\x1b[0m — agent multiplexer${state.runtimeLabel ? ` \x1b[32m● ${state.runtimeLabel}\x1b[0m` : ""}`;
427
+ const divider = state.isDevRuntime
428
+ ? `\x1b[33m${"─".repeat(Math.max(0, cols))}\x1b[0m`
429
+ : "".repeat(Math.max(0, cols));
430
+ const header = ["", centerInBlock(title), divider, ""];
431
431
  const content = [];
432
432
  const operationFailures = state.operationFailures ?? [];
433
433
  if (operationFailures.length > 0) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll } from "vitest";
5
+ const aimuxTestHome = mkdtempSync(join(tmpdir(), "aimux-vitest-home-"));
6
+ process.env.AIMUX_HOME = aimuxTestHome;
7
+ afterAll(() => {
8
+ rmSync(aimuxTestHome, { recursive: true, force: true });
9
+ });
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::-ms-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;-webkit-font-feature-settings:normal;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{-webkit-font-feature-settings:normal;font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{-webkit-font-feature-settings:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-webkit-input-placeholder{opacity:1;color:#9ca3af}textarea::-webkit-input-placeholder{opacity:1;color:#9ca3af}:is(input:-ms-placeholder-shown,textarea:-ms-placeholder-shown){opacity:1;color:#9ca3af}:is(input:placeholder-shown,textarea:placeholder-shown){opacity:1;color:#9ca3af}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--css-interop-darkMode:class dark;--css-interop:true;--css-interop-nativewind:true;--background:0 0% 100%;--foreground:240 10% 3.9%;--card:0 0% 100%;--card-foreground:240 10% 3.9%;--primary:240 5.9% 10%;--primary-foreground:0 0% 98%;--secondary:240 4.8% 95.9%;--secondary-foreground:240 5.9% 10%;--muted:240 4.8% 95.9%;--muted-foreground:240 3.8% 46.1%;--accent:240 4.8% 95.9%;--accent-foreground:240 5.9% 10%;--destructive:0 84.2% 60.2%;--destructive-foreground:0 0% 98%;--border:240 5.9% 90%;--input:240 5.9% 90%;--ring:240 5.9% 10%}.dark:root{--background:240 10% 3.9%;--foreground:0 0% 98%;--card:240 6% 10%;--card-foreground:0 0% 98%;--primary:0 0% 98%;--primary-foreground:240 5.9% 10%;--secondary:240 5% 13%;--secondary-foreground:0 0% 98%;--muted:240 5% 13%;--muted-foreground:240 5% 64.9%;--accent:240 5% 16%;--accent-foreground:0 0% 98%;--destructive:0 62.8% 30.6%;--destructive-foreground:0 0% 98%;--border:240 4% 22%;--input:240 4% 22%;--ring:240 4.9% 83.9%}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.right-0{right:0}.right-1{right:.25rem}.right-2{right:.5rem}.top-0{top:0}.top-1{top:.25rem}.top-full{top:100%}.z-30{z-index:30}.z-50{z-index:50}.m-4{margin:1rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.-ml-1{margin-left:-.25rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.ml-2{margin-left:.5rem}.ml-2\.5{margin-left:.625rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-\[20px\]{margin-left:20px}.ml-\[22px\]{margin-left:22px}.ml-auto{margin-left:auto}.mr-1\.5{margin-right:.375rem}.mr-2{margin-right:.5rem}.mr-2\.5{margin-right:.625rem}.mr-3{margin-right:.75rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-7{margin-top:1.75rem}.inline{display:inline}.flex{display:flex}.table{display:table}.h-0\.5{height:.125rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[6px\]{height:6px}.h-\[7px\]{height:7px}.h-full{height:100%}.h-px{height:1px}.max-h-32{max-height:8rem}.max-h-\[620px\]{max-height:620px}.min-h-11{min-height:2.75rem}.min-h-\[220px\]{min-height:220px}.min-h-\[400px\]{min-height:400px}.min-h-\[40px\]{min-height:40px}.min-h-\[420px\]{min-height:420px}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-\[13px\]{width:13px}.w-\[14px\]{width:14px}.w-\[6px\]{width:6px}.w-\[7px\]{width:7px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0}.min-w-\[112px\]{min-width:112px}.min-w-\[18px\]{min-width:18px}.min-w-\[62px\]{min-width:62px}.max-w-\[280px\]{max-width:280px}.max-w-\[360px\]{max-width:360px}.max-w-\[380px\]{max-width:380px}.max-w-\[400px\]{max-width:400px}.max-w-\[420px\]{max-width:420px}.max-w-\[80\%\]{max-width:80%}.max-w-\[90\%\]{max-width:90%}.flex-1{flex:1}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.rotate-45{--tw-rotate:45deg;-webkit-transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{-webkit-transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-\[1\.5px\]{border-radius:1.5px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-\[1\.5px\]{border-width:1.5px}.border-b{border-bottom-width:1px}.border-b-\[1\.5px\]{border-bottom-width:1.5px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-\[\#2a2b31\]{--tw-border-opacity:1;border-color:rgb(42 43 49/var(--tw-border-opacity,1))}.border-\[\#3a3c44\]{--tw-border-opacity:1;border-color:rgb(58 60 68/var(--tw-border-opacity,1))}.border-\[\#44464e\]{--tw-border-opacity:1;border-color:rgb(68 70 78/var(--tw-border-opacity,1))}.border-\[\#4ade80\]{--tw-border-opacity:1;border-color:rgb(74 222 128/var(--tw-border-opacity,1))}.border-\[\#6b6d75\]{--tw-border-opacity:1;border-color:rgb(107 109 117/var(--tw-border-opacity,1))}.border-\[\#edeef0\]{--tw-border-opacity:1;border-color:rgb(237 238 240/var(--tw-border-opacity,1))}.border-amber-400{--tw-border-opacity:1;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-amber-500\/30{border-color:#f59e0b4d}.border-amber-500\/40{border-color:#f59e0b66}.border-border{border-color:hsl(var(--border))}.border-destructive\/50{border-color:hsl(var(--destructive) / .5)}.border-emerald-500\/40{border-color:#10b98166}.border-foreground{border-color:hsl(var(--foreground))}.border-primary-foreground\/35{border-color:hsl(var(--primary-foreground) / .35)}.border-red-500\/40{border-color:#ef444466}.border-ring{border-color:hsl(var(--ring))}.border-sky-500\/40{border-color:#0ea5e966}.border-transparent{border-color:#0000}.border-violet-500\/40{border-color:#8b5cf666}.bg-\[\#161719\]{--tw-bg-opacity:1;background-color:rgb(22 23 25/var(--tw-bg-opacity,1))}.bg-\[\#1f2025\]{--tw-bg-opacity:1;background-color:rgb(31 32 37/var(--tw-bg-opacity,1))}.bg-\[\#26272d\]{--tw-bg-opacity:1;background-color:rgb(38 39 45/var(--tw-bg-opacity,1))}.bg-\[\#4ade80\]{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-\[\#5b5d66\]{--tw-bg-opacity:1;background-color:rgb(91 93 102/var(--tw-bg-opacity,1))}.bg-accent{background-color:hsl(var(--accent))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-500{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/15{background-color:#f59e0b26}.bg-background{background-color:hsl(var(--background))}.bg-background\/60{background-color:hsl(var(--background) / .6)}.bg-background\/90{background-color:hsl(var(--background) / .9)}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-emerald-500\/15{background-color:#10b98126}.bg-foreground{background-color:hsl(var(--foreground))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity,1))}.bg-muted{background-color:hsl(var(--muted))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary-foreground\/15{background-color:hsl(var(--primary-foreground) / .15)}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-950\/20{background-color:#450a0a33}.bg-secondary{background-color:hsl(var(--secondary))}.bg-secondary\/40{background-color:hsl(var(--secondary) / .4)}.bg-sky-500\/10{background-color:#0ea5e91a}.bg-transparent{background-color:#0000}.bg-violet-500\/10{background-color:#8b5cf61a}.bg-zinc-400{--tw-bg-opacity:1;background-color:rgb(161 161 170/var(--tw-bg-opacity,1))}.bg-zinc-500{--tw-bg-opacity:1;background-color:rgb(113 113 122/var(--tw-bg-opacity,1))}.bg-zinc-500\/10{background-color:#71717a1a}.bg-zinc-500\/15{background-color:#71717a26}.bg-zinc-600{--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity,1))}.p-0{padding:0}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-2{padding-bottom:.5rem}.pb-3\.5{padding-bottom:.875rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pr-4{padding-right:1rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[18px\]{font-size:18px}.text-\[22px\]{font-size:22px}.text-\[28px\]{font-size:28px}.text-\[32px\]{font-size:32px}.text-\[44px\]{font-size:44px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-\[0\.5em\]{letter-spacing:.5em}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-\[\#4ade80\]{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-\[\#787a83\]{--tw-text-opacity:1;color:rgb(120 122 131/var(--tw-text-opacity,1))}.text-\[\#a6a8b0\]{--tw-text-opacity:1;color:rgb(166 168 176/var(--tw-text-opacity,1))}.text-\[\#edeef0\]{--tw-text-opacity:1;color:rgb(237 238 240/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-amber-500{--tw-text-opacity:1;color:rgb(245 158 11/var(--tw-text-opacity,1))}.text-background{color:hsl(var(--background))}.text-background\/70{color:hsl(var(--background) / .7)}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-foreground{color:hsl(var(--foreground))}.text-foreground\/90{color:hsl(var(--foreground) / .9)}.text-green-800{--tw-text-opacity:1;color:rgb(22 101 52/var(--tw-text-opacity,1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/70{color:hsl(var(--muted-foreground) / .7)}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-muted-foreground{-webkit-text-decoration-color:hsl(var(--muted-foreground));text-decoration-color:hsl(var(--muted-foreground))}.opacity-50{opacity:.5}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow,0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;scrollbar-width:thin}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial}}body{background-color:#fff}.dark body{background-color:#09090b}body.has-navbar:before{content:"";z-index:-1;pointer-events:none;background-color:#f2f2f2;border-bottom:1px solid #e4e4e7;height:137px;position:fixed;top:0;left:0;right:0}.dark body.has-navbar:before{background-color:#010101;border-bottom-color:#27272a}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#8080804d;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#80808080}.placeholder\:text-muted-foreground::-webkit-input-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground:-ms-placeholder-shown{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.last\:border-b-0:last-child{border-bottom-width:0}.hover\:bg-\[\#232429\]:hover{--tw-bg-opacity:1;background-color:rgb(35 36 41/var(--tw-bg-opacity,1))}.active\:bg-\[\#26272d\]:active{--tw-bg-opacity:1;background-color:rgb(38 39 45/var(--tw-bg-opacity,1))}.active\:bg-accent:active{background-color:hsl(var(--accent))}.active\:bg-accent\/50:active{background-color:hsl(var(--accent) / .5)}.active\:bg-accent\/60:active{background-color:hsl(var(--accent) / .6)}.active\:opacity-70:active{opacity:.7}.active\:opacity-80:active{opacity:.8}.disabled\:opacity-40:disabled{opacity:.4}.dark\:bg-green-900:is(.dark *){--tw-bg-opacity:1;background-color:rgb(20 83 45/var(--tw-bg-opacity,1))}.dark\:text-green-200:is(.dark *){--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}@media (width>=768px){.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:py-7{padding-top:1.75rem;padding-bottom:1.75rem}}@media (width>=1024px){.lg\:w-\[320px\]{width:320px}.lg\:flex-row{flex-direction:row}}