mtech-ai-local-runner 0.1.107 → 0.1.108
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 +6 -0
- package/dist/src/menubar/channel.js +19 -0
- package/dist/src/menubar/launcher.js +22 -1
- package/dist/src/menubar/wiring.js +30 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.1.108: the menu bar app reconnects to a new runner
|
|
2
|
+
|
|
3
|
+
After the daemon opens the menu bar app it waits up to 10 s for it to connect; if it does not (an app already
|
|
4
|
+
running keeps the socket it was first given), the daemon quits it and opens it once more. Nothing else changes from
|
|
5
|
+
0.1.107.
|
|
6
|
+
|
|
1
7
|
## 0.1.107: the AI Gateway menu bar app
|
|
2
8
|
|
|
3
9
|
`setup` installs `~/Applications/AI Gateway.app` and the daemon opens it (Gateway Work Item b9218937). A bell in the
|
|
@@ -21,6 +21,7 @@ export class MenubarChannel {
|
|
|
21
21
|
state;
|
|
22
22
|
waiting = new Map();
|
|
23
23
|
listeners = { refresh: [], quit: [] };
|
|
24
|
+
appWaiters = new Set();
|
|
24
25
|
constructor(options) {
|
|
25
26
|
this.options = options;
|
|
26
27
|
this.state = { ...options.initial };
|
|
@@ -31,6 +32,22 @@ export class MenubarChannel {
|
|
|
31
32
|
on(event, listener) {
|
|
32
33
|
this.listeners[event].push(listener);
|
|
33
34
|
}
|
|
35
|
+
/** True once an app is connected, at once when one already is; false when none connects within timeoutMs. */
|
|
36
|
+
waitForApp(timeoutMs) {
|
|
37
|
+
if (this.connected)
|
|
38
|
+
return Promise.resolve(true);
|
|
39
|
+
return new Promise(resolve => {
|
|
40
|
+
const settle = (connected) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
this.appWaiters.delete(onConnect);
|
|
43
|
+
resolve(connected);
|
|
44
|
+
};
|
|
45
|
+
const onConnect = () => settle(true);
|
|
46
|
+
const timer = setTimeout(() => settle(this.connected), timeoutMs);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
this.appWaiters.add(onConnect);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
34
51
|
async start() {
|
|
35
52
|
if (this.server !== null)
|
|
36
53
|
throw new Error("MENUBAR_CHANNEL_ALREADY_LISTENING");
|
|
@@ -128,6 +145,8 @@ export class MenubarChannel {
|
|
|
128
145
|
socket.once("close", () => { if (this.app === socket)
|
|
129
146
|
this.app = null; });
|
|
130
147
|
socket.write(stateLine(this.state));
|
|
148
|
+
for (const waiter of [...this.appWaiters])
|
|
149
|
+
waiter();
|
|
131
150
|
}
|
|
132
151
|
handle(message) {
|
|
133
152
|
if (message === null)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
|
-
import { installedAppPath, menubarRemovedByPerson, runFile } from "./install.js";
|
|
2
|
+
import { MENUBAR_EXECUTABLE, installedAppPath, menubarRemovedByPerson, runFile } from "./install.js";
|
|
3
3
|
/**
|
|
4
4
|
* Opens the installed app in the background (Gateway Work Item b9218937), unless the person removed it. An app that
|
|
5
5
|
* already runs stays as it is. Never throws.
|
|
@@ -20,3 +20,24 @@ export async function launchMenubarApp(input) {
|
|
|
20
20
|
return "skipped";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Quits a running app and waits, up to 3 s, until it is gone, so a following `open` starts a new instance that
|
|
25
|
+
* receives its arguments instead of bringing the old one forward. Never throws.
|
|
26
|
+
*/
|
|
27
|
+
export async function quitMenubarApp(input = {}) {
|
|
28
|
+
if ((input.platform ?? process.platform) !== "darwin")
|
|
29
|
+
return;
|
|
30
|
+
const run = input.run ?? runFile;
|
|
31
|
+
try {
|
|
32
|
+
await run("/usr/bin/pkill", ["-x", MENUBAR_EXECUTABLE]);
|
|
33
|
+
for (let i = 0; i < 15; i++) {
|
|
34
|
+
// pgrep exits 1 once no process has the name.
|
|
35
|
+
if ((await run("/usr/bin/pgrep", ["-x", MENUBAR_EXECUTABLE])).code !== 0)
|
|
36
|
+
return;
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* Quitting is best effort; the launch that follows still runs. */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { packageVersion } from "../package-version.js";
|
|
2
2
|
import { MenubarChannel, menubarSocketPath } from "./channel.js";
|
|
3
|
-
import { launchMenubarApp } from "./launcher.js";
|
|
3
|
+
import { launchMenubarApp, quitMenubarApp } from "./launcher.js";
|
|
4
4
|
import { MenubarRuntime } from "./runtime.js";
|
|
5
|
+
/** How long the daemon waits for the app it opened to connect before it reopens it once. */
|
|
6
|
+
export const APP_CONNECT_TIMEOUT_MS = 10_000;
|
|
5
7
|
/**
|
|
6
8
|
* The menu bar parts of one daemon (Gateway Work Item b9218937). A channel that cannot start is logged and left off:
|
|
7
9
|
* the daemon and its osascript notices run as before.
|
|
@@ -13,6 +15,30 @@ export function buildMenubar(input) {
|
|
|
13
15
|
const runtime = new MenubarRuntime({ channel, gatewayMcpUrl: input.gatewayMcpUrl, runnerVersion: packageVersion(),
|
|
14
16
|
readInbox: input.readInbox, rescanRooms: input.rescanRooms, log: input.log });
|
|
15
17
|
channel.on("quit", () => input.log({ event: "menubar_app_quit" }));
|
|
18
|
+
const launch = input.launch ?? (() => launchMenubarApp({ home: input.home, stateRoot: input.stateRoot, socketPath }));
|
|
19
|
+
const quit = input.quit ?? (() => quitMenubarApp());
|
|
20
|
+
const timeoutMs = input.connectTimeoutMs ?? APP_CONNECT_TIMEOUT_MS;
|
|
21
|
+
let closed = false;
|
|
22
|
+
// An app that already runs keeps the socket it was first given, and `open` passes no new arguments to it. One that
|
|
23
|
+
// was opened but does not connect is therefore quit and opened again, once.
|
|
24
|
+
const ensureApp = async () => {
|
|
25
|
+
const status = await launch();
|
|
26
|
+
if (status !== "launched") {
|
|
27
|
+
input.log({ event: "menubar_app", status });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (await channel.waitForApp(timeoutMs)) {
|
|
31
|
+
input.log({ event: "menubar_app", status, connected: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (closed)
|
|
35
|
+
return;
|
|
36
|
+
await quit();
|
|
37
|
+
if (closed)
|
|
38
|
+
return;
|
|
39
|
+
const again = await launch();
|
|
40
|
+
input.log({ event: "menubar_app_relaunched", status: again, connected: again === "launched" && await channel.waitForApp(timeoutMs) });
|
|
41
|
+
};
|
|
16
42
|
return {
|
|
17
43
|
runtime,
|
|
18
44
|
channel,
|
|
@@ -25,9 +51,11 @@ export function buildMenubar(input) {
|
|
|
25
51
|
return;
|
|
26
52
|
}
|
|
27
53
|
runtime.start();
|
|
28
|
-
|
|
54
|
+
// In the background: the daemon's loops never wait for the app.
|
|
55
|
+
void ensureApp().catch(() => input.log({ event: "menubar_app_failed" }));
|
|
29
56
|
},
|
|
30
57
|
close: async () => {
|
|
58
|
+
closed = true;
|
|
31
59
|
runtime.stop();
|
|
32
60
|
await channel.stop();
|
|
33
61
|
},
|