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.
- package/README.md +13 -4
- package/bin/aimux +4 -0
- package/bin/aimux-dev +2 -6
- package/dist/agent-output-parser-audit.d.ts +23 -0
- package/dist/agent-output-parser-audit.js +187 -0
- package/dist/agent-output-parser-contract.d.ts +9 -0
- package/dist/agent-output-parser-contract.js +33 -0
- package/dist/agent-output-parser-fixtures.d.ts +15 -0
- package/dist/agent-output-parser-fixtures.js +593 -0
- package/dist/agent-output-parser-harness.d.ts +21 -0
- package/dist/agent-output-parser-harness.js +43 -0
- package/dist/agent-output-parser-test-utils.d.ts +1 -0
- package/dist/agent-output-parser-test-utils.js +7 -0
- package/dist/agent-output-parser.js +215 -35
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +69 -4
- package/dist/attachment-store.d.ts +7 -0
- package/dist/attachment-store.js +64 -5
- package/dist/backend-session-discovery.d.ts +17 -0
- package/dist/backend-session-discovery.js +57 -0
- package/dist/config.js +9 -4
- package/dist/connection-targets.js +20 -1
- package/dist/context/context-bridge.js +4 -1
- package/dist/credentials.js +3 -6
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +16 -0
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/targets.js +14 -2
- package/dist/dashboard/ui-state-store.js +4 -3
- package/dist/last-used.js +3 -2
- package/dist/launcher-env.d.ts +4 -0
- package/dist/launcher-env.js +70 -0
- package/dist/main.js +16 -1
- package/dist/metadata-server.d.ts +13 -2
- package/dist/metadata-server.js +60 -4
- package/dist/metadata-store.js +4 -3
- package/dist/mobile-push-bridge.d.ts +8 -0
- package/dist/mobile-push-bridge.js +22 -0
- package/dist/mobile-push-throttle.d.ts +23 -0
- package/dist/mobile-push-throttle.js +53 -0
- package/dist/multiplexer/dashboard-model.js +3 -2
- package/dist/multiplexer/dashboard-ops.d.ts +3 -2
- package/dist/multiplexer/dashboard-ops.js +2 -2
- package/dist/multiplexer/dashboard-tail-methods.d.ts +3 -2
- package/dist/multiplexer/dashboard-tail-methods.js +2 -2
- package/dist/multiplexer/dashboard-view-methods.js +2 -0
- package/dist/multiplexer/index.d.ts +1 -1
- package/dist/multiplexer/index.js +4 -4
- package/dist/multiplexer/persistence-methods.js +2 -1
- package/dist/multiplexer/runtime-lifecycle-methods.js +6 -2
- package/dist/multiplexer/runtime-state.js +13 -1
- package/dist/multiplexer/service-state-snapshot.js +4 -2
- package/dist/multiplexer/services.js +5 -4
- package/dist/multiplexer/session-launch.d.ts +1 -1
- package/dist/multiplexer/session-launch.js +18 -6
- package/dist/multiplexer/session-runtime-core.js +9 -2
- package/dist/multiplexer/tool-picker.d.ts +2 -1
- package/dist/multiplexer/tool-picker.js +29 -21
- package/dist/notify.d.ts +1 -1
- package/dist/notify.js +8 -5
- package/dist/paths.js +50 -4
- package/dist/project-takeover.d.ts +1 -0
- package/dist/project-takeover.js +117 -0
- package/dist/relay-client.d.ts +10 -0
- package/dist/relay-client.js +5 -0
- package/dist/runtime-core/backend-id-reconcile.d.ts +13 -0
- package/dist/runtime-core/backend-id-reconcile.js +23 -0
- package/dist/runtime-core/exchange-store.js +3 -8
- package/dist/runtime-core/topology-store.js +3 -8
- package/dist/runtime-owner.d.ts +3 -0
- package/dist/runtime-owner.js +10 -0
- package/dist/shell-args.d.ts +13 -0
- package/dist/shell-args.js +25 -0
- package/dist/shell-hooks.d.ts +1 -0
- package/dist/shell-hooks.js +1 -0
- package/dist/team.js +4 -3
- package/dist/tmux/runtime-manager.js +2 -0
- package/dist/tui/screens/dashboard-renderers.js +6 -6
- package/dist/vitest.setup.d.ts +1 -0
- package/dist/vitest.setup.js +9 -0
- package/dist-ui/_expo/static/css/web-8782287775683e5a944b821b854d0f60.css +1 -0
- package/dist-ui/_expo/static/js/web/{entry-477c745b2adc79367a4380ecf07d9ff6.js → entry-90d00d223eefabe5cc21e4329b274fa5.js} +260 -252
- package/dist-ui/index.html +2 -2
- package/package.json +3 -1
- package/dist-ui/_expo/static/css/web-30453ede1678c16acb08b97e83e8646d.css +0 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
1
3
|
const PROD_WEB_APP_URL = "https://aimux.app";
|
|
2
4
|
const PROD_RELAY_URL = "wss://relay.aimux.app";
|
|
3
5
|
const DEV_WEB_APP_URL = "http://localhost:8081";
|
|
6
|
+
const DEV_HOME = join(homedir(), ".aimux-dev");
|
|
7
|
+
const DEV_DAEMON_PORT = "43191";
|
|
4
8
|
function cleanUrl(value) {
|
|
5
9
|
return value.replace(/\/$/, "");
|
|
6
10
|
}
|
|
@@ -8,8 +12,23 @@ function optionalEnv(value) {
|
|
|
8
12
|
const trimmed = value?.trim();
|
|
9
13
|
return trimmed ? trimmed : undefined;
|
|
10
14
|
}
|
|
15
|
+
function normalizeHome(value) {
|
|
16
|
+
const trimmed = value?.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return null;
|
|
19
|
+
if (trimmed === "~")
|
|
20
|
+
return homedir();
|
|
21
|
+
if (trimmed.startsWith("~/"))
|
|
22
|
+
return resolve(homedir(), trimmed.slice(2));
|
|
23
|
+
return resolve(trimmed);
|
|
24
|
+
}
|
|
25
|
+
// Mirrors launcher-env's lane detection: any one dev-lane signal marks the
|
|
26
|
+
// runtime as development, so the label survives a single var being unset.
|
|
11
27
|
export function isDevelopmentRuntime() {
|
|
12
|
-
return process.env.AIMUX_ENV === "development"
|
|
28
|
+
return (process.env.AIMUX_ENV?.trim() === "development" ||
|
|
29
|
+
normalizeHome(process.env.AIMUX_HOME) === DEV_HOME ||
|
|
30
|
+
process.env.AIMUX_DAEMON_PORT?.trim() === DEV_DAEMON_PORT ||
|
|
31
|
+
optionalEnv(process.env.AIMUX_WEB_APP_URL) === DEV_WEB_APP_URL);
|
|
13
32
|
}
|
|
14
33
|
export function resolveWebAppUrl(override) {
|
|
15
34
|
const value = optionalEnv(override) ??
|
|
@@ -9,6 +9,7 @@ import { algorithmicCompact } from "./compactor.js";
|
|
|
9
9
|
import { debugTurn, debugGit, debugContext, debugCompact } from "../debug.js";
|
|
10
10
|
import { TmuxRuntimeManager } from "../tmux/runtime-manager.js";
|
|
11
11
|
import { classifyToolPane } from "../tool-output-watchers.js";
|
|
12
|
+
import { parseAgentOutput } from "../agent-output-parser.js";
|
|
12
13
|
const git = simpleGit();
|
|
13
14
|
const MAX_LIVE_MD_BYTES = 50 * 1024;
|
|
14
15
|
const MAX_LIVE_MD_LINES = 200;
|
|
@@ -243,7 +244,9 @@ export class ContextWatcher {
|
|
|
243
244
|
debugContext("wrote", `${session.id}/live.md`, snapshot.length);
|
|
244
245
|
const turns = readHistory(session.id, { lastN: 1 });
|
|
245
246
|
if (promptVisible) {
|
|
246
|
-
const
|
|
247
|
+
const blocks = parseAgentOutput(normalized, { tool: session.command }).blocks;
|
|
248
|
+
const lastResponseBlock = blocks.filter((block) => block.type === "response").at(-1);
|
|
249
|
+
const snapshotTurn = lastResponseBlock?.text.trim() ?? "";
|
|
247
250
|
if (snapshotTurn) {
|
|
248
251
|
const lastTurn = turns.at(-1);
|
|
249
252
|
if (!lastTurn || lastTurn.content !== snapshotTurn) {
|
package/dist/credentials.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Populated by `aimux login` (browser flow). The daemon reads this to connect
|
|
4
4
|
// to the relay. The token is a long-lived relay-signed JWT — not a Clerk
|
|
5
5
|
// session token — so it survives daemon restarts and runs for ~90 days.
|
|
6
|
-
import { existsSync,
|
|
7
|
-
import {
|
|
6
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
7
|
+
import { atomicWrite } from "./atomic-write.js";
|
|
8
8
|
import { getAuthPath } from "./paths.js";
|
|
9
9
|
export function loadCredentials() {
|
|
10
10
|
const path = getAuthPath();
|
|
@@ -21,10 +21,7 @@ export function loadCredentials() {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
export function saveCredentials(creds) {
|
|
24
|
-
|
|
25
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
26
|
-
writeFileSync(path, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
27
|
-
chmodSync(path, 0o600);
|
|
24
|
+
atomicWrite(getAuthPath(), `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
28
25
|
}
|
|
29
26
|
export function clearCredentials() {
|
|
30
27
|
const path = getAuthPath();
|
package/dist/daemon.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare function projectServiceStatus(projectRoot: string): Promise<Proje
|
|
|
30
30
|
export declare class AimuxDaemon {
|
|
31
31
|
private server;
|
|
32
32
|
private relayClient;
|
|
33
|
+
private readonly pushThrottle;
|
|
33
34
|
private readonly children;
|
|
34
35
|
private readonly projectEnsurePromises;
|
|
35
36
|
private state;
|
package/dist/daemon.js
CHANGED
|
@@ -9,6 +9,7 @@ import { loadMetadataEndpoint } from "./metadata-store.js";
|
|
|
9
9
|
import { requestJson } from "./http-client.js";
|
|
10
10
|
import { getLoggingConfig, log } from "./debug.js";
|
|
11
11
|
import { RelayClient } from "./relay-client.js";
|
|
12
|
+
import { MobilePushThrottle } from "./mobile-push-throttle.js";
|
|
12
13
|
import { loadCredentials, setRemoteEnabled } from "./credentials.js";
|
|
13
14
|
import { assertRemoteAccessAllowed, parseRemoteActor } from "./remote-access.js";
|
|
14
15
|
const DEFAULT_DAEMON_PORT = 43190;
|
|
@@ -328,6 +329,7 @@ export async function projectServiceStatus(projectRoot) {
|
|
|
328
329
|
export class AimuxDaemon {
|
|
329
330
|
server = null;
|
|
330
331
|
relayClient = null;
|
|
332
|
+
pushThrottle = new MobilePushThrottle();
|
|
331
333
|
children = new Map();
|
|
332
334
|
projectEnsurePromises = new Map();
|
|
333
335
|
state = loadDaemonState();
|
|
@@ -663,6 +665,20 @@ export class AimuxDaemon {
|
|
|
663
665
|
if (method === "POST" && pathname === "/relay/disable") {
|
|
664
666
|
return { status: 200, body: { ok: true, relay: this.disableRelay() } };
|
|
665
667
|
}
|
|
668
|
+
if (method === "POST" && pathname === "/internal/push") {
|
|
669
|
+
if (actor)
|
|
670
|
+
return { status: 403, body: { ok: false, error: "internal route is loopback-only" } };
|
|
671
|
+
const payload = body;
|
|
672
|
+
if (!payload?.title)
|
|
673
|
+
return { status: 400, body: { ok: false, error: "title is required" } };
|
|
674
|
+
if (this.relayClient?.getStatus().status !== "connected") {
|
|
675
|
+
return { status: 200, body: { ok: true, suppressed: true, reason: "relay_unavailable" } };
|
|
676
|
+
}
|
|
677
|
+
if (!this.pushThrottle.allow(payload))
|
|
678
|
+
return { status: 200, body: { ok: true, suppressed: true } };
|
|
679
|
+
this.relayClient.pushNotification(payload);
|
|
680
|
+
return { status: 200, body: { ok: true } };
|
|
681
|
+
}
|
|
666
682
|
if (method === "GET" && pathname === "/projects") {
|
|
667
683
|
const liveById = this.state.projects;
|
|
668
684
|
const projects = listDesktopProjects().map((project) => ({
|
|
@@ -128,6 +128,7 @@ export interface DashboardViewModel {
|
|
|
128
128
|
selectedServiceId?: string;
|
|
129
129
|
selectedTeammates: DashboardSession[];
|
|
130
130
|
runtimeLabel?: string;
|
|
131
|
+
isDevRuntime?: boolean;
|
|
131
132
|
mainCheckout: MainCheckoutInfo;
|
|
132
133
|
worktreeRemoval?: DashboardWorktreeRemovalInfo;
|
|
133
134
|
operationFailures: DashboardOperationFailure[];
|
package/dist/dashboard/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { isDashboardWindowName } from "../tmux/runtime-manager.js";
|
|
2
2
|
import { getDashboardCommandSpec } from "./command-spec.js";
|
|
3
|
+
import { getRuntimeOwnerId, TMUX_DASHBOARD_OWNER_OPTION, TMUX_RUNTIME_OWNER_OPTION } from "../runtime-owner.js";
|
|
3
4
|
function isUsableDashboardTarget(tmux, projectRoot, dashboardBuildStamp, dashboardTarget) {
|
|
4
5
|
const currentBuildStamp = tmux.getWindowOption(dashboardTarget, "@aimux-dashboard-build");
|
|
6
|
+
const currentOwner = getRuntimeOwnerId();
|
|
7
|
+
const targetRuntimeOwner = tmux.getSessionOption(dashboardTarget.sessionName, TMUX_RUNTIME_OWNER_OPTION);
|
|
8
|
+
const targetDashboardOwner = tmux.getWindowOption(dashboardTarget, TMUX_DASHBOARD_OWNER_OPTION);
|
|
5
9
|
const targetProjectRoot = tmux.getSessionOption(dashboardTarget.sessionName, "@aimux-project-root");
|
|
6
10
|
const paneCommand = tmux.displayMessage("#{pane_current_command}", dashboardTarget.windowId);
|
|
7
11
|
const paneTail = paneCommand === "bash" ? tmux.captureTarget(dashboardTarget, { startLine: -40 }) : "";
|
|
8
12
|
return (tmux.isWindowAlive(dashboardTarget) &&
|
|
9
13
|
targetProjectRoot === projectRoot &&
|
|
14
|
+
targetRuntimeOwner === currentOwner &&
|
|
15
|
+
targetDashboardOwner === currentOwner &&
|
|
10
16
|
currentBuildStamp === dashboardBuildStamp &&
|
|
11
17
|
paneCommand !== "cat" &&
|
|
12
18
|
paneCommand !== "tail" &&
|
|
@@ -110,11 +116,17 @@ export function resolveDashboardTarget(projectRoot, tmux, options = {}) {
|
|
|
110
116
|
const openSessionName = tmux.getOpenSessionName(dashboardSession.sessionName, tmux.isInsideTmux());
|
|
111
117
|
const dashboardTarget = tmux.ensureDashboardWindow(openSessionName, projectRoot, dashboardCommand);
|
|
112
118
|
const currentBuildStamp = tmux.getWindowOption(dashboardTarget, "@aimux-dashboard-build");
|
|
113
|
-
const
|
|
119
|
+
const currentOwner = getRuntimeOwnerId();
|
|
120
|
+
const currentDashboardOwner = tmux.getWindowOption(dashboardTarget, TMUX_DASHBOARD_OWNER_OPTION);
|
|
121
|
+
const shouldRespawn = options.forceReload === true ||
|
|
122
|
+
!tmux.isWindowAlive(dashboardTarget) ||
|
|
123
|
+
currentBuildStamp !== dashboardBuildStamp ||
|
|
124
|
+
currentDashboardOwner !== currentOwner;
|
|
114
125
|
if (shouldRespawn) {
|
|
115
126
|
tmux.respawnWindow(dashboardTarget, dashboardCommand);
|
|
116
|
-
tmux.setWindowOption(dashboardTarget, "@aimux-dashboard-build", dashboardBuildStamp);
|
|
117
127
|
}
|
|
128
|
+
tmux.setWindowOption(dashboardTarget, "@aimux-dashboard-build", dashboardBuildStamp);
|
|
129
|
+
tmux.setWindowOption(dashboardTarget, TMUX_DASHBOARD_OWNER_OPTION, currentOwner);
|
|
118
130
|
return { dashboardSession, dashboardTarget };
|
|
119
131
|
}
|
|
120
132
|
export function openDashboardTarget(projectRoot, tmux, options = {}) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { writeJsonAtomic } from "../atomic-write.js";
|
|
2
3
|
import { dashboardOrderKey, moveDashboardOrder, orderDashboardServicesForWorktree, orderDashboardSessionsForWorktree, orderDashboardWorktreeGroups, } from "./order.js";
|
|
3
4
|
import { getDashboardClientUiStatePath, getDashboardUiStatePath } from "../paths.js";
|
|
4
5
|
export class DashboardUiStateStore {
|
|
@@ -89,8 +90,8 @@ export class DashboardUiStateStore {
|
|
|
89
90
|
this.flatSessionId = flatSession.id;
|
|
90
91
|
}
|
|
91
92
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
writeJsonAtomic(getDashboardUiStatePath(), sharedSnapshot);
|
|
94
|
+
writeJsonAtomic(getDashboardClientUiStatePath(clientKey), clientSnapshot);
|
|
94
95
|
}
|
|
95
96
|
catch { }
|
|
96
97
|
}
|
package/dist/last-used.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { getProjectStateDirFor } from "./paths.js";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { parseRecencyTimestamp } from "./recency.js";
|
|
5
|
+
import { writeJsonAtomic } from "./atomic-write.js";
|
|
5
6
|
const LAST_USED_VERSION = 1;
|
|
6
7
|
const MAX_RECENT_IDS = 64;
|
|
7
8
|
const EMPTY_STATE = {
|
|
@@ -66,7 +67,7 @@ export function compareLastUsed(left, right, rankMap) {
|
|
|
66
67
|
function persistLastUsedState(projectRoot, state) {
|
|
67
68
|
const dir = getProjectStateDirFor(projectRoot);
|
|
68
69
|
mkdirSync(dir, { recursive: true });
|
|
69
|
-
|
|
70
|
+
writeJsonAtomic(getLastUsedPath(projectRoot), state);
|
|
70
71
|
}
|
|
71
72
|
function normalizeLastUsedState(state) {
|
|
72
73
|
const items = Object.fromEntries(Object.entries(state.items ?? {}).flatMap(([itemId, value]) => typeof value?.lastUsedAt === "string" ? [[itemId, { lastUsedAt: value.lastUsedAt }]] : []));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
const PROD_HOME = join(homedir(), ".aimux");
|
|
4
|
+
const DEV_HOME = join(homedir(), ".aimux-dev");
|
|
5
|
+
const PROD_DAEMON_PORT = "43190";
|
|
6
|
+
const DEV_DAEMON_PORT = "43191";
|
|
7
|
+
const PROD_WEB_APP_URL = "https://aimux.app";
|
|
8
|
+
const DEV_WEB_APP_URL = "http://localhost:8081";
|
|
9
|
+
function normalizeHome(value) {
|
|
10
|
+
const trimmed = value?.trim();
|
|
11
|
+
if (!trimmed)
|
|
12
|
+
return null;
|
|
13
|
+
if (trimmed === "~")
|
|
14
|
+
return homedir();
|
|
15
|
+
if (trimmed.startsWith("~/"))
|
|
16
|
+
return resolve(homedir(), trimmed.slice(2));
|
|
17
|
+
return resolve(trimmed);
|
|
18
|
+
}
|
|
19
|
+
function sameHome(value, expected) {
|
|
20
|
+
const normalized = normalizeHome(value);
|
|
21
|
+
return normalized !== null && normalized === expected;
|
|
22
|
+
}
|
|
23
|
+
function sameValue(value, expected) {
|
|
24
|
+
return value?.trim() === expected;
|
|
25
|
+
}
|
|
26
|
+
function blank(value) {
|
|
27
|
+
return !value?.trim();
|
|
28
|
+
}
|
|
29
|
+
function clearSessionScopedEnv(env) {
|
|
30
|
+
delete env.AIMUX_METADATA_ENDPOINT_FILE;
|
|
31
|
+
delete env.AIMUX_SESSION_ID;
|
|
32
|
+
delete env.AIMUX_SHELL_INTEGRATION_SCRIPT;
|
|
33
|
+
delete env.AIMUX_TOOL;
|
|
34
|
+
}
|
|
35
|
+
export function prepareStableCliEnv(env = process.env) {
|
|
36
|
+
const inheritedDevTarget = sameHome(env.AIMUX_HOME, DEV_HOME) ||
|
|
37
|
+
sameValue(env.AIMUX_DAEMON_PORT, DEV_DAEMON_PORT) ||
|
|
38
|
+
sameValue(env.AIMUX_ENV, "development") ||
|
|
39
|
+
sameValue(env.AIMUX_WEB_APP_URL, DEV_WEB_APP_URL);
|
|
40
|
+
if (blank(env.AIMUX_HOME) || sameHome(env.AIMUX_HOME, DEV_HOME))
|
|
41
|
+
env.AIMUX_HOME = PROD_HOME;
|
|
42
|
+
if (blank(env.AIMUX_DAEMON_PORT) || sameValue(env.AIMUX_DAEMON_PORT, DEV_DAEMON_PORT)) {
|
|
43
|
+
env.AIMUX_DAEMON_PORT = PROD_DAEMON_PORT;
|
|
44
|
+
}
|
|
45
|
+
if (blank(env.AIMUX_ENV) || sameValue(env.AIMUX_ENV, "development"))
|
|
46
|
+
env.AIMUX_ENV = "production";
|
|
47
|
+
if (blank(env.AIMUX_WEB_APP_URL) || sameValue(env.AIMUX_WEB_APP_URL, DEV_WEB_APP_URL)) {
|
|
48
|
+
env.AIMUX_WEB_APP_URL = PROD_WEB_APP_URL;
|
|
49
|
+
}
|
|
50
|
+
if (inheritedDevTarget)
|
|
51
|
+
clearSessionScopedEnv(env);
|
|
52
|
+
}
|
|
53
|
+
export function prepareDevCliEnv(env = process.env) {
|
|
54
|
+
const inheritedStableTarget = sameHome(env.AIMUX_HOME, PROD_HOME) ||
|
|
55
|
+
sameValue(env.AIMUX_DAEMON_PORT, PROD_DAEMON_PORT) ||
|
|
56
|
+
sameValue(env.AIMUX_ENV, "production") ||
|
|
57
|
+
sameValue(env.AIMUX_WEB_APP_URL, PROD_WEB_APP_URL);
|
|
58
|
+
if (blank(env.AIMUX_HOME) || sameHome(env.AIMUX_HOME, PROD_HOME))
|
|
59
|
+
env.AIMUX_HOME = DEV_HOME;
|
|
60
|
+
if (blank(env.AIMUX_DAEMON_PORT) || sameValue(env.AIMUX_DAEMON_PORT, PROD_DAEMON_PORT)) {
|
|
61
|
+
env.AIMUX_DAEMON_PORT = DEV_DAEMON_PORT;
|
|
62
|
+
}
|
|
63
|
+
if (blank(env.AIMUX_ENV) || sameValue(env.AIMUX_ENV, "production"))
|
|
64
|
+
env.AIMUX_ENV = "development";
|
|
65
|
+
if (blank(env.AIMUX_WEB_APP_URL) || sameValue(env.AIMUX_WEB_APP_URL, PROD_WEB_APP_URL)) {
|
|
66
|
+
env.AIMUX_WEB_APP_URL = DEV_WEB_APP_URL;
|
|
67
|
+
}
|
|
68
|
+
if (inheritedStableTarget)
|
|
69
|
+
clearSessionScopedEnv(env);
|
|
70
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import { createThread, listThreadSummaries, markThreadSeen, readMessages, readTh
|
|
|
20
20
|
import { sendDirectMessage, sendThreadMessage } from "./orchestration.js";
|
|
21
21
|
import { runLoginFlow } from "./login-flow.js";
|
|
22
22
|
import { clearCredentials, loadCredentials, setRemoteEnabled } from "./credentials.js";
|
|
23
|
+
import { takeOverProjectFromOtherOwners } from "./project-takeover.js";
|
|
23
24
|
import { acceptHandoff, approveReview, acceptTask, assignTask, blockTask, completeHandoff, completeTask, reopenTask, requestTaskChanges, sendHandoff, } from "./orchestration-actions.js";
|
|
24
25
|
import { readAllTasks, readTask } from "./tasks.js";
|
|
25
26
|
import { clearNotifications, listNotifications, markNotificationsRead, upsertNotification, unreadNotificationCount, } from "./notifications.js";
|
|
@@ -37,6 +38,7 @@ import { persistProjectRuntimeSnapshotsBeforeTmuxStop } from "./multiplexer/serv
|
|
|
37
38
|
import { configureLogging, log, resolveLoggingRuntimeConfig } from "./debug.js";
|
|
38
39
|
import { createRuntimeTopologyStore } from "./runtime-core/topology-store.js";
|
|
39
40
|
import { listTopologySessionStates } from "./runtime-core/topology-sessions.js";
|
|
41
|
+
import { reconcileOfflineBackendSessionIds } from "./runtime-core/backend-id-reconcile.js";
|
|
40
42
|
import { listTopologyWorktreeGraveyard, listTopologyWorktreeGraveyardPaths, } from "./runtime-core/topology-worktrees.js";
|
|
41
43
|
import { buildRuntimeMigrationReport, importRuntimeMigration, renderRuntimeMigrationImportResult, renderRuntimeMigrationReport, renderRuntimeMigrationRollbackResult, rollbackRuntimeMigration, } from "./runtime-migration.js";
|
|
42
44
|
import { DEFAULT_LOCAL_UI_HOST, DEFAULT_LOCAL_UI_PORT, openUrlInBrowser, startLocalUiServer, } from "./local-ui-server.js";
|
|
@@ -561,6 +563,8 @@ program
|
|
|
561
563
|
const tmux = new TmuxRuntimeManager();
|
|
562
564
|
ensureTmuxAvailable(tmux);
|
|
563
565
|
if (!tool && !opts.resume && !opts.restore) {
|
|
566
|
+
await takeOverProjectFromOtherOwners(projectRoot);
|
|
567
|
+
await ensureDaemonProjectReady(projectRoot);
|
|
564
568
|
const liveDashboard = findLiveDashboardTarget(projectRoot, tmux);
|
|
565
569
|
if (liveDashboard) {
|
|
566
570
|
tmux.openTarget(liveDashboard.dashboardTarget, {
|
|
@@ -2790,16 +2794,23 @@ repairCmd
|
|
|
2790
2794
|
const tmux = new TmuxRuntimeManager();
|
|
2791
2795
|
ensureTmuxAvailable(tmux);
|
|
2792
2796
|
const result = repairTmuxRuntime(tmux, { projectRoot });
|
|
2797
|
+
const backendReconcile = reconcileOfflineBackendSessionIds(projectRoot);
|
|
2793
2798
|
if (opts.open) {
|
|
2794
2799
|
const { dashboardTarget } = resolveDashboardTarget(projectRoot, tmux);
|
|
2795
2800
|
tmux.openTarget(dashboardTarget, { insideTmux: tmux.isInsideTmux(), alreadyResolved: true });
|
|
2796
2801
|
exitAfterOpen();
|
|
2797
2802
|
}
|
|
2798
2803
|
if (opts.json) {
|
|
2799
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2804
|
+
console.log(JSON.stringify({ ...result, backendReconcile }, null, 2));
|
|
2800
2805
|
return;
|
|
2801
2806
|
}
|
|
2802
2807
|
console.log(renderTmuxRepairResult(result));
|
|
2808
|
+
if (backendReconcile.reconciled.length > 0) {
|
|
2809
|
+
console.log(`Recovered backend session id for ${backendReconcile.reconciled.length} offline agent(s):`);
|
|
2810
|
+
for (const entry of backendReconcile.reconciled) {
|
|
2811
|
+
console.log(` ${entry.id} -> ${entry.backendSessionId}`);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2803
2814
|
});
|
|
2804
2815
|
const metadataCmd = program.command("metadata").description("Push metadata into aimux tmux status integration");
|
|
2805
2816
|
const metadataTracker = new AgentTracker();
|
|
@@ -2927,6 +2938,10 @@ program
|
|
|
2927
2938
|
const result = { ok: true, action, sessionId };
|
|
2928
2939
|
if (payload.session_id) {
|
|
2929
2940
|
result.backendSessionId = payload.session_id;
|
|
2941
|
+
// No local fallback: this hook is a short-lived CLI process, not the
|
|
2942
|
+
// runtime that owns the session, so it cannot record into topology
|
|
2943
|
+
// directly. A service-down capture gap is closed by reconcile-on-restart.
|
|
2944
|
+
await postLiveProjectServiceJsonOrLocal(projectRoot, "/agents/record-backend-session", { sessionId, backendSessionId: payload.session_id }, () => ({ ok: true })).catch(() => { });
|
|
2930
2945
|
}
|
|
2931
2946
|
const setActivity = async (activity) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-activity", { session: sessionId, activity }, () => metadataTracker.setActivity(sessionId, activity, projectRoot));
|
|
2932
2947
|
const setAttention = async (attention) => postLiveProjectServiceJsonOrLocal(projectRoot, "/set-attention", { session: sessionId, attention }, () => metadataTracker.setAttention(sessionId, attention, projectRoot));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SessionAlertDisplayContext } from "./alert-display.js";
|
|
2
2
|
import { type MessageKind } from "./threads.js";
|
|
3
3
|
import { type TaskLifecycleResult } from "./orchestration-actions.js";
|
|
4
|
+
import type { LaunchOverride } from "./shell-args.js";
|
|
4
5
|
import type { ParsedAgentOutput } from "./agent-output-parser.js";
|
|
5
6
|
import { ProjectEventBus } from "./project-events.js";
|
|
6
7
|
interface MetadataServerOptions {
|
|
@@ -204,7 +205,7 @@ interface MetadataServerOptions {
|
|
|
204
205
|
sessionId?: string;
|
|
205
206
|
worktreePath?: string;
|
|
206
207
|
open?: boolean;
|
|
207
|
-
|
|
208
|
+
launchOverride?: LaunchOverride;
|
|
208
209
|
}) => Promise<{
|
|
209
210
|
sessionId: string;
|
|
210
211
|
}> | {
|
|
@@ -242,7 +243,7 @@ interface MetadataServerOptions {
|
|
|
242
243
|
instruction?: string;
|
|
243
244
|
worktreePath?: string;
|
|
244
245
|
open?: boolean;
|
|
245
|
-
|
|
246
|
+
launchOverride?: LaunchOverride;
|
|
246
247
|
}) => Promise<{
|
|
247
248
|
sessionId: string;
|
|
248
249
|
threadId: string;
|
|
@@ -297,6 +298,16 @@ interface MetadataServerOptions {
|
|
|
297
298
|
status: "graveyard";
|
|
298
299
|
previousStatus: "running" | "offline";
|
|
299
300
|
};
|
|
301
|
+
recordBackendSessionId?: (input: {
|
|
302
|
+
sessionId: string;
|
|
303
|
+
backendSessionId: string;
|
|
304
|
+
}) => Promise<{
|
|
305
|
+
sessionId: string;
|
|
306
|
+
backendSessionId: string;
|
|
307
|
+
}> | {
|
|
308
|
+
sessionId: string;
|
|
309
|
+
backendSessionId: string;
|
|
310
|
+
};
|
|
300
311
|
sendAgentInput?: (input: {
|
|
301
312
|
sessionId: string;
|
|
302
313
|
text: string;
|
package/dist/metadata-server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getDashboardClientUiStatePath, getPlansDir, getProjectId, getProjectStateDir } from "./paths.js";
|
|
6
|
+
import { writeJsonAtomic } from "./atomic-write.js";
|
|
6
7
|
import { updateSessionMetadata, clearSessionLogs, saveMetadataEndpoint, loadMetadataState, } from "./metadata-store.js";
|
|
7
8
|
import { contextualizeAlertInput, mergeDisplayContext, metadataDisplayContext, } from "./alert-display.js";
|
|
8
9
|
import { notifyAlert } from "./notify.js";
|
|
@@ -16,7 +17,7 @@ import { readAllTasks, readTask } from "./tasks.js";
|
|
|
16
17
|
import { buildWorkflowEntries } from "./workflow.js";
|
|
17
18
|
import { markLastUsed } from "./last-used.js";
|
|
18
19
|
import { formatRelativeRecency } from "./recency.js";
|
|
19
|
-
import { getAttachment, getAttachmentContent } from "./attachment-store.js";
|
|
20
|
+
import { createUploadedAttachment, getAttachment, getAttachmentContent, getAttachmentRecord, } from "./attachment-store.js";
|
|
20
21
|
import { ProjectEventBus } from "./project-events.js";
|
|
21
22
|
import { getProjectServiceManifest } from "./project-service-manifest.js";
|
|
22
23
|
import { applyShellStateTransition } from "./shell-state.js";
|
|
@@ -78,7 +79,7 @@ function persistDashboardClientPreference(clientSession, update) {
|
|
|
78
79
|
}
|
|
79
80
|
catch { }
|
|
80
81
|
update(snapshot);
|
|
81
|
-
|
|
82
|
+
writeJsonAtomic(path, snapshot);
|
|
82
83
|
}
|
|
83
84
|
function persistDashboardReturnSelection(tmux, projectRoot, currentClientSession, currentWindowId) {
|
|
84
85
|
persistDashboardClientPreference(currentClientSession, (snapshot) => {
|
|
@@ -177,6 +178,16 @@ function sendBytes(res, status, body, mimeType) {
|
|
|
177
178
|
res.setHeader("connection", "close");
|
|
178
179
|
res.end(body);
|
|
179
180
|
}
|
|
181
|
+
function formatAgentInputWithAttachments(text, attachments) {
|
|
182
|
+
const trimmedText = text.trim();
|
|
183
|
+
if (attachments.length === 0)
|
|
184
|
+
return text;
|
|
185
|
+
const body = trimmedText || "Please review the attached image file(s).";
|
|
186
|
+
const attachmentLines = attachments.map((attachment) => {
|
|
187
|
+
return `- ${attachment.filename} (${attachment.mimeType}, ${attachment.sizeBytes} bytes): ${attachment.contentPath}`;
|
|
188
|
+
});
|
|
189
|
+
return `${body}\n\nAttached image files:\n${attachmentLines.join("\n")}`;
|
|
190
|
+
}
|
|
180
191
|
function sendSseEvent(res, event, data) {
|
|
181
192
|
res.write(`event: ${event}\n`);
|
|
182
193
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
@@ -1937,6 +1948,17 @@ export class MetadataServer {
|
|
|
1937
1948
|
send(res, 200, { ok: true, ...result });
|
|
1938
1949
|
return;
|
|
1939
1950
|
}
|
|
1951
|
+
if (req.method === "POST" && url.pathname === "/agents/record-backend-session") {
|
|
1952
|
+
const body = (await readJson(req));
|
|
1953
|
+
if (!this.options.lifecycle?.recordBackendSessionId) {
|
|
1954
|
+
send(res, 501, { ok: false, error: "backend session recording not supported by this service" });
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const result = await this.options.lifecycle.recordBackendSessionId(body);
|
|
1958
|
+
this.options.onChange?.();
|
|
1959
|
+
send(res, 200, { ok: true, ...result });
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1940
1962
|
if (req.method === "POST" && url.pathname === "/agents/interrupt") {
|
|
1941
1963
|
const body = (await readJson(req));
|
|
1942
1964
|
if (!this.options.lifecycle?.interruptAgent) {
|
|
@@ -1989,19 +2011,53 @@ export class MetadataServer {
|
|
|
1989
2011
|
return;
|
|
1990
2012
|
}
|
|
1991
2013
|
const text = typeof body.text === "string" ? body.text : "";
|
|
1992
|
-
|
|
2014
|
+
const attachmentIds = Array.isArray(body.attachmentIds)
|
|
2015
|
+
? body.attachmentIds
|
|
2016
|
+
.filter((id) => typeof id === "string")
|
|
2017
|
+
.map((id) => id.trim())
|
|
2018
|
+
.filter(Boolean)
|
|
2019
|
+
: [];
|
|
2020
|
+
if (!text.trim() && attachmentIds.length === 0) {
|
|
1993
2021
|
send(res, 400, { ok: false, error: "text is required" });
|
|
1994
2022
|
return;
|
|
1995
2023
|
}
|
|
2024
|
+
const attachments = attachmentIds.map((id) => getAttachmentRecord(id));
|
|
2025
|
+
const missingAttachmentId = attachmentIds.find((_, index) => attachments[index] === null);
|
|
2026
|
+
if (missingAttachmentId) {
|
|
2027
|
+
send(res, 400, { ok: false, error: `attachment not found: ${missingAttachmentId}` });
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
1996
2030
|
if (!this.options.lifecycle?.sendAgentInput) {
|
|
1997
2031
|
send(res, 501, { ok: false, error: "agent input not supported by this service" });
|
|
1998
2032
|
return;
|
|
1999
2033
|
}
|
|
2000
|
-
const
|
|
2034
|
+
const formattedText = formatAgentInputWithAttachments(text, attachments.filter((entry) => !!entry));
|
|
2035
|
+
const result = await this.options.lifecycle.sendAgentInput({ sessionId, text: formattedText });
|
|
2001
2036
|
this.options.onChange?.();
|
|
2002
2037
|
send(res, 200, { ok: true, ...result });
|
|
2003
2038
|
return;
|
|
2004
2039
|
}
|
|
2040
|
+
if (req.method === "POST" && url.pathname === "/attachments") {
|
|
2041
|
+
const body = (await readJson(req));
|
|
2042
|
+
if (typeof body.filename !== "string" ||
|
|
2043
|
+
typeof body.mimeType !== "string" ||
|
|
2044
|
+
typeof body.dataBase64 !== "string") {
|
|
2045
|
+
send(res, 400, { ok: false, error: "filename, mimeType, and dataBase64 are required" });
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
try {
|
|
2049
|
+
const attachment = createUploadedAttachment({
|
|
2050
|
+
filename: body.filename,
|
|
2051
|
+
mimeType: body.mimeType,
|
|
2052
|
+
dataBase64: body.dataBase64,
|
|
2053
|
+
});
|
|
2054
|
+
send(res, 200, { ok: true, attachment });
|
|
2055
|
+
}
|
|
2056
|
+
catch (error) {
|
|
2057
|
+
send(res, 400, { ok: false, error: error instanceof Error ? error.message : "invalid attachment" });
|
|
2058
|
+
}
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2005
2061
|
const attachmentContentMatch = url.pathname.match(/^\/attachments\/([^/]+)\/content$/);
|
|
2006
2062
|
if (req.method === "GET" && attachmentContentMatch) {
|
|
2007
2063
|
const content = getAttachmentContent(decodeURIComponent(attachmentContentMatch[1] || ""));
|
package/dist/metadata-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { writeJsonAtomic } from "./atomic-write.js";
|
|
3
|
+
import { quarantineCorruptFile, writeJsonAtomic, writeTextAtomic } from "./atomic-write.js";
|
|
4
4
|
import { getProjectStateDir, getProjectStateDirFor } from "./paths.js";
|
|
5
5
|
function ensureParent(path) {
|
|
6
6
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -21,6 +21,7 @@ function loadJson(path, fallback) {
|
|
|
21
21
|
return JSON.parse(readFileSync(path, "utf-8"));
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
|
+
quarantineCorruptFile(path);
|
|
24
25
|
return fallback;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -94,7 +95,7 @@ export function saveMetadataEndpoint(endpoint, projectRoot) {
|
|
|
94
95
|
saveJson(endpointPathFor(projectRoot), endpoint);
|
|
95
96
|
const textPath = endpointTextPathFor(projectRoot);
|
|
96
97
|
ensureParent(textPath);
|
|
97
|
-
|
|
98
|
+
writeTextAtomic(textPath, `http://${endpoint.host}:${endpoint.port}\n`);
|
|
98
99
|
}
|
|
99
100
|
export function removeMetadataEndpoint(projectRoot) {
|
|
100
101
|
try {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AlertEvent } from "./project-events.js";
|
|
2
|
+
/**
|
|
3
|
+
* Forwards an alert that already passed desktop notification gating to the
|
|
4
|
+
* daemon, which relays it to the owner's mobile devices. Fire-and-forget: the
|
|
5
|
+
* daemon owns the single relay connection, so the project service hands off and
|
|
6
|
+
* never blocks the alert path on push delivery.
|
|
7
|
+
*/
|
|
8
|
+
export declare function forwardAlertToMobilePush(event: AlertEvent): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { requestDaemonJson } from "./daemon.js";
|
|
2
|
+
/**
|
|
3
|
+
* Forwards an alert that already passed desktop notification gating to the
|
|
4
|
+
* daemon, which relays it to the owner's mobile devices. Fire-and-forget: the
|
|
5
|
+
* daemon owns the single relay connection, so the project service hands off and
|
|
6
|
+
* never blocks the alert path on push delivery.
|
|
7
|
+
*/
|
|
8
|
+
export function forwardAlertToMobilePush(event) {
|
|
9
|
+
void requestDaemonJson("/internal/push", {
|
|
10
|
+
method: "POST",
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
title: event.title || "aimux",
|
|
13
|
+
body: event.message || event.sessionId || event.kind,
|
|
14
|
+
kind: event.kind,
|
|
15
|
+
sessionId: event.sessionId,
|
|
16
|
+
projectId: event.projectId,
|
|
17
|
+
projectRoot: process.cwd(),
|
|
18
|
+
dedupeKey: event.dedupeKey,
|
|
19
|
+
}),
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
}).catch(() => { });
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ThrottleInput {
|
|
2
|
+
dedupeKey?: string;
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
kind?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
body?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* In-memory guard for outbound mobile pushes. Collapses identical alerts
|
|
10
|
+
* re-emitted within a TTL window (chatty idle/needs_input polling) and caps the
|
|
11
|
+
* push rate per session so one runaway agent cannot flood the device.
|
|
12
|
+
*/
|
|
13
|
+
export declare class MobilePushThrottle {
|
|
14
|
+
private readonly dedupeTtlMs;
|
|
15
|
+
private readonly sessionLimit;
|
|
16
|
+
private readonly sessionWindowMs;
|
|
17
|
+
private readonly now;
|
|
18
|
+
private readonly lastByKey;
|
|
19
|
+
private readonly sessionHits;
|
|
20
|
+
constructor(dedupeTtlMs?: number, sessionLimit?: number, sessionWindowMs?: number, now?: () => number);
|
|
21
|
+
allow(input: ThrottleInput): boolean;
|
|
22
|
+
private prune;
|
|
23
|
+
}
|