talon-agent 1.40.2 → 1.41.0
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/package.json
CHANGED
|
@@ -18,6 +18,9 @@ export const meshHandlers: SharedActionHandlers = {
|
|
|
18
18
|
get_device_history: (body) =>
|
|
19
19
|
getMeshService().deviceHistory(body.device, body.hours),
|
|
20
20
|
ring_device: (body) => getMeshService().ringDevice(body.device, body.message),
|
|
21
|
+
// Registry hygiene: drop a stale/superseded device entry. Requires an
|
|
22
|
+
// explicit target — a destructive action must never default-pick a device.
|
|
23
|
+
remove_device: (body) => getMeshService().removeDevice(body.device),
|
|
21
24
|
get_device_status: (body) => getMeshService().getDeviceStatus(body.device),
|
|
22
25
|
// Exec + filesystem surface — the substrate teleport routes through.
|
|
23
26
|
device_exec: (body) =>
|
|
@@ -91,10 +91,48 @@ export class MeshRegistry {
|
|
|
91
91
|
now,
|
|
92
92
|
);
|
|
93
93
|
this.devices.set(device.id, device);
|
|
94
|
+
// Stale-duplicate eviction. A reinstall (or an old app build) carries its
|
|
95
|
+
// own persisted device id, so the same physical machine re-registers
|
|
96
|
+
// under a fresh identity and the old entry lingers forever as an offline
|
|
97
|
+
// ghost. Same name + platform under a different id, currently OFFLINE →
|
|
98
|
+
// superseded install, drop it. An online doppelganger is kept: two live
|
|
99
|
+
// devices can legitimately share a name.
|
|
100
|
+
let evicted = false;
|
|
101
|
+
for (const [id, d] of this.devices) {
|
|
102
|
+
if (id === device.id) continue;
|
|
103
|
+
if (d.name !== device.name || d.platform !== device.platform) continue;
|
|
104
|
+
if (toDeviceInfo(d, now, PRESENCE_TIMEOUT_MS).online) continue;
|
|
105
|
+
this.devices.delete(id);
|
|
106
|
+
this.locations.delete(id);
|
|
107
|
+
this.history.delete(id);
|
|
108
|
+
evicted = true;
|
|
109
|
+
}
|
|
94
110
|
await this.persistDevices();
|
|
111
|
+
if (evicted) {
|
|
112
|
+
await this.persistLocations();
|
|
113
|
+
await this.persistHistory();
|
|
114
|
+
}
|
|
95
115
|
return device;
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Drop a device (and its location + history) from the registry. Returns
|
|
120
|
+
* the removed device, or undefined when the id is unknown. Note a still-
|
|
121
|
+
* connected companion re-registers on its next heartbeat — removal is for
|
|
122
|
+
* stale entries.
|
|
123
|
+
*/
|
|
124
|
+
async removeDevice(deviceId: string): Promise<DeviceInfo | undefined> {
|
|
125
|
+
const device = this.devices.get(deviceId);
|
|
126
|
+
if (!device) return undefined;
|
|
127
|
+
this.devices.delete(deviceId);
|
|
128
|
+
const hadLocation = this.locations.delete(deviceId);
|
|
129
|
+
const hadHistory = this.history.delete(deviceId);
|
|
130
|
+
await this.persistDevices();
|
|
131
|
+
if (hadLocation) await this.persistLocations();
|
|
132
|
+
if (hadHistory) await this.persistHistory();
|
|
133
|
+
return toDeviceInfo(device, device.lastSeen);
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
async storeLocation(
|
|
99
137
|
body: Record<string, unknown>,
|
|
100
138
|
now = Date.now(),
|
package/src/core/mesh/service.ts
CHANGED
|
@@ -340,6 +340,36 @@ export class MeshService {
|
|
|
340
340
|
return { ok: true, text: this.locationSummary(target, loc) };
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* `remove_device`: drop a stale device from the mesh registry (with its
|
|
345
|
+
* location + history). Destructive, so it requires an explicit target —
|
|
346
|
+
* no "default to the most recent device" like the read tools.
|
|
347
|
+
*/
|
|
348
|
+
async removeDevice(query?: unknown): Promise<MeshToolResult> {
|
|
349
|
+
await this.load();
|
|
350
|
+
if (typeof query !== "string" || !query.trim()) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
text: "remove_device needs an explicit device id or name — there is no default target for a destructive operation. See list_devices.",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const resolved = this.resolveDevice(query);
|
|
357
|
+
if ("error" in resolved) return { ok: false, text: resolved.error };
|
|
358
|
+
const target = resolved.target;
|
|
359
|
+
const removed = await this.registry.removeDevice(target.id);
|
|
360
|
+
if (!removed) {
|
|
361
|
+
return { ok: false, text: this.noSuchDevice(query).text };
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
ok: true,
|
|
365
|
+
text:
|
|
366
|
+
`Removed ${removed.name} [id: ${removed.id}] (${removed.platform}, last seen ${age(Date.now() - removed.lastSeen)}) from the mesh registry.` +
|
|
367
|
+
(target.online
|
|
368
|
+
? " Note: it was still online — a connected companion re-registers within ~60s, so quit the app first if it keeps coming back."
|
|
369
|
+
: ""),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
343
373
|
/** `ring_device`: make the device sound/vibrate so it can be found. */
|
|
344
374
|
ringDevice(query?: unknown, message?: unknown): Promise<MeshToolResult> {
|
|
345
375
|
return this.commandTool(query, "ring", {
|
package/src/core/tools/mesh.ts
CHANGED
|
@@ -60,6 +60,20 @@ export const meshTools: ToolDefinition[] = [
|
|
|
60
60
|
execute: (params, bridge) => bridge("ring_device", params),
|
|
61
61
|
tag: "mesh",
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: "remove_device",
|
|
65
|
+
description:
|
|
66
|
+
"Remove a stale device from the mesh registry (with its stored location and history). Use for superseded installs and old duplicates — e.g. a reinstalled companion that re-registered under a new id, leaving the old entry as an offline ghost. Requires an explicit device id or name (no default target). Removing a still-connected device is pointless: it re-registers on its next heartbeat within ~60s.",
|
|
67
|
+
schema: {
|
|
68
|
+
device: z
|
|
69
|
+
.string()
|
|
70
|
+
.describe(
|
|
71
|
+
"Device id, exact name, or unique name fragment of the entry to remove (see list_devices). Prefer the id when duplicates share a name.",
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
execute: (params, bridge) => bridge("remove_device", params),
|
|
75
|
+
tag: "mesh",
|
|
76
|
+
},
|
|
63
77
|
{
|
|
64
78
|
name: "get_device_status",
|
|
65
79
|
description:
|
|
@@ -71,7 +85,7 @@ export const meshTools: ToolDefinition[] = [
|
|
|
71
85
|
{
|
|
72
86
|
name: "device_exec",
|
|
73
87
|
description:
|
|
74
|
-
"Run a shell command ON a Talon companion device (e.g. the phone) and return its stdout/stderr/exit code. The device executes it in its own sandbox (Android: app UID, or elevated via Shizuku if available). Use for on-device tasks like tidying a folder. Prefer `teleport` for a sustained session.",
|
|
88
|
+
"Run a shell command ON a Talon companion device (e.g. the phone) and return its stdout/stderr/exit code. The device executes it in its own sandbox (Android: app UID, or elevated via Shizuku if available). Use for on-device tasks like tidying a folder. Prefer `teleport` for a sustained session. For persistent streams (log tailing, watchers), background with output redirected to a file — `cmd > /tmp/out.log 2>&1 &` returns immediately; poll the file with device_read_file.",
|
|
75
89
|
schema: {
|
|
76
90
|
device: deviceParam,
|
|
77
91
|
cmd: z.string().describe("The shell command to run on the device."),
|