@alook/daemon 0.0.156 → 0.0.157
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/dist/cli/index.js +143 -35
- package/dist/index.js +110 -15
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -19,6 +19,35 @@ function proxyServerApiFromEnv(prefix = "ALOOK", env = process.env) {
|
|
|
19
19
|
function createProxyServerApi(config) {
|
|
20
20
|
const fetchImpl = config.fetchImpl ?? fetch;
|
|
21
21
|
const base = config.proxyUrl.replace(/\/+$/, "");
|
|
22
|
+
async function parseJsonResponse(res, method) {
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
text = await res.text();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
28
|
+
throw new Error(`upstream body read failed from /api/${method} (${res.status}): ${cause}`);
|
|
29
|
+
}
|
|
30
|
+
if (text.length === 0) {
|
|
31
|
+
if (res.ok)
|
|
32
|
+
return;
|
|
33
|
+
throw new Error(`upstream returned ${res.status} with non-JSON body from /api/${method}`);
|
|
34
|
+
}
|
|
35
|
+
let json;
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(`upstream returned ${res.status} with non-JSON body from /api/${method}`);
|
|
40
|
+
}
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const e = new Error(json?.error ?? `proxy api/${method} failed (${res.status})`);
|
|
43
|
+
if (json?.code !== undefined)
|
|
44
|
+
e.code = json.code;
|
|
45
|
+
if (json?.hint !== undefined)
|
|
46
|
+
e.hint = json.hint;
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
22
51
|
async function call(method, body) {
|
|
23
52
|
const { agentId: _omit, ...wire } = body ?? {};
|
|
24
53
|
const res = await fetchImpl(`${base}/api/${method}`, {
|
|
@@ -29,14 +58,7 @@ function createProxyServerApi(config) {
|
|
|
29
58
|
},
|
|
30
59
|
body: JSON.stringify(wire)
|
|
31
60
|
});
|
|
32
|
-
|
|
33
|
-
if (!res.ok) {
|
|
34
|
-
const e = new Error(json?.error ?? `proxy api/${method} failed (${res.status})`);
|
|
35
|
-
e.code = json?.code;
|
|
36
|
-
e.hint = json?.hint;
|
|
37
|
-
throw e;
|
|
38
|
-
}
|
|
39
|
-
return json;
|
|
61
|
+
return parseJsonResponse(res, method);
|
|
40
62
|
}
|
|
41
63
|
async function callUpload(req) {
|
|
42
64
|
const form = new FormData;
|
|
@@ -49,13 +71,7 @@ function createProxyServerApi(config) {
|
|
|
49
71
|
headers: { authorization: `Bearer ${config.voucher}` },
|
|
50
72
|
body: form
|
|
51
73
|
});
|
|
52
|
-
|
|
53
|
-
if (!res.ok) {
|
|
54
|
-
const e = new Error(json?.error ?? `proxy api/attachmentUpload failed (${res.status})`);
|
|
55
|
-
e.code = json?.code;
|
|
56
|
-
throw e;
|
|
57
|
-
}
|
|
58
|
-
return json;
|
|
74
|
+
return parseJsonResponse(res, "attachmentUpload");
|
|
59
75
|
}
|
|
60
76
|
async function callDownload(req) {
|
|
61
77
|
const res = await fetchImpl(`${base}/api/attachmentDownload`, {
|
|
@@ -67,10 +83,8 @@ function createProxyServerApi(config) {
|
|
|
67
83
|
body: JSON.stringify({ id: req.id })
|
|
68
84
|
});
|
|
69
85
|
if (!res.ok) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
e.code = body?.code;
|
|
73
|
-
throw e;
|
|
86
|
+
await parseJsonResponse(res, "attachmentDownload");
|
|
87
|
+
throw new Error("unreachable: parseJsonResponse must throw on !res.ok");
|
|
74
88
|
}
|
|
75
89
|
const encoded = res.headers.get("x-alook-filename");
|
|
76
90
|
const filename = encoded ? decodeURIComponent(encoded) : path.basename(req.destPath);
|
|
@@ -225,6 +239,12 @@ class WsControlChannel {
|
|
|
225
239
|
async reportAgentActivity(info) {
|
|
226
240
|
this.sendFrame({ type: "agent_activity", ...info });
|
|
227
241
|
}
|
|
242
|
+
reportAgentTyping(info) {
|
|
243
|
+
this.sendFrame({ type: "agent_typing", ...info });
|
|
244
|
+
}
|
|
245
|
+
reportAgentTypingStop(info) {
|
|
246
|
+
this.sendFrame({ type: "agent_typing_stop", ...info });
|
|
247
|
+
}
|
|
228
248
|
async reportBotAuditEvent(frame) {
|
|
229
249
|
this.sendFrame(frame);
|
|
230
250
|
}
|
|
@@ -1631,6 +1651,8 @@ class AgentRouter {
|
|
|
1631
1651
|
latestSeq: cmd.unreadNotice.latestSeq
|
|
1632
1652
|
});
|
|
1633
1653
|
try {
|
|
1654
|
+
const beforeStatus = this.opts.manager.snapshot?.().agents?.[cmd.agentId]?.status ?? "unregistered";
|
|
1655
|
+
const wasActive = this.opts.typingTracker?.hasAny(cmd.agentId) ?? false;
|
|
1634
1656
|
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
1635
1657
|
this.opts.manager.register(cmd.agentId, {
|
|
1636
1658
|
runtimeConfig: cmd.config,
|
|
@@ -1638,8 +1660,17 @@ class AgentRouter {
|
|
|
1638
1660
|
launchId: cmd.launchId
|
|
1639
1661
|
});
|
|
1640
1662
|
this.running.add(cmd.agentId);
|
|
1663
|
+
const dmScope = cmd.unreadNotice.dmConversationId;
|
|
1664
|
+
if (dmScope)
|
|
1665
|
+
this.opts.typingTracker?.add(cmd.agentId, dmScope);
|
|
1641
1666
|
const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
|
|
1642
1667
|
this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
|
|
1668
|
+
if (dmScope && wasActive && beforeStatus === "running") {
|
|
1669
|
+
this.opts.channel.reportAgentTyping?.({
|
|
1670
|
+
agentId: cmd.agentId,
|
|
1671
|
+
dmConversationId: dmScope
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1643
1674
|
await this.opts.channel.reportWakeAck?.({
|
|
1644
1675
|
agentId: cmd.agentId,
|
|
1645
1676
|
launchId: cmd.launchId,
|
|
@@ -1720,6 +1751,31 @@ class AgentRouter {
|
|
|
1720
1751
|
}
|
|
1721
1752
|
}
|
|
1722
1753
|
}
|
|
1754
|
+
// src/manager/typingScopeTracker.ts
|
|
1755
|
+
function createTypingScopeTracker() {
|
|
1756
|
+
const scopes = new Map;
|
|
1757
|
+
return {
|
|
1758
|
+
add(agentId, dmConversationId) {
|
|
1759
|
+
let set = scopes.get(agentId);
|
|
1760
|
+
if (!set) {
|
|
1761
|
+
set = new Set;
|
|
1762
|
+
scopes.set(agentId, set);
|
|
1763
|
+
}
|
|
1764
|
+
set.add(dmConversationId);
|
|
1765
|
+
},
|
|
1766
|
+
snapshot(agentId) {
|
|
1767
|
+
const set = scopes.get(agentId);
|
|
1768
|
+
return set ? [...set] : [];
|
|
1769
|
+
},
|
|
1770
|
+
hasAny(agentId) {
|
|
1771
|
+
const set = scopes.get(agentId);
|
|
1772
|
+
return !!set && set.size > 0;
|
|
1773
|
+
},
|
|
1774
|
+
clear(agentId) {
|
|
1775
|
+
scopes.delete(agentId);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1723
1779
|
// src/timeline/timeline.ts
|
|
1724
1780
|
import { appendFileSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, renameSync as renameSync2, existsSync } from "fs";
|
|
1725
1781
|
import { join as join2 } from "path";
|
|
@@ -2057,7 +2113,7 @@ function serversSection() {
|
|
|
2057
2113
|
return [
|
|
2058
2114
|
"## Servers",
|
|
2059
2115
|
"",
|
|
2060
|
-
`If a message contains a \`/
|
|
2116
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces an owner-only check for you — it only accepts an invite your owner created, and " + "rejects anything else with a clear reason. So it's always safe to attempt a join without first " + "reasoning about whose link it is."
|
|
2061
2117
|
].join(`
|
|
2062
2118
|
`);
|
|
2063
2119
|
}
|
|
@@ -2385,16 +2441,6 @@ function writeAgentFile(workDir, systemPromptContent) {
|
|
|
2385
2441
|
}
|
|
2386
2442
|
|
|
2387
2443
|
// src/drivers/cliTransport.ts
|
|
2388
|
-
var DEFAULT_ACTIVE_CAPABILITIES = [
|
|
2389
|
-
"send",
|
|
2390
|
-
"read",
|
|
2391
|
-
"mentions",
|
|
2392
|
-
"tasks",
|
|
2393
|
-
"reactions",
|
|
2394
|
-
"server",
|
|
2395
|
-
"channels",
|
|
2396
|
-
"knowledge"
|
|
2397
|
-
];
|
|
2398
2444
|
var DEFAULT_CLI_CONFIG = {
|
|
2399
2445
|
cliName: "alook",
|
|
2400
2446
|
envPrefix: "ALOOK",
|
|
@@ -2406,7 +2452,6 @@ function resolveStateHome(envPrefix) {
|
|
|
2406
2452
|
async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG, platform = process.platform) {
|
|
2407
2453
|
const E = cli.envPrefix;
|
|
2408
2454
|
const stateHome = resolveStateHome(E);
|
|
2409
|
-
const capabilities = cli.activeCapabilities ?? DEFAULT_ACTIVE_CAPABILITIES;
|
|
2410
2455
|
const stateDir = path4.join(ctx.workingDirectory, cli.stateDirName);
|
|
2411
2456
|
await fs5.promises.mkdir(stateDir, { recursive: true });
|
|
2412
2457
|
if (ctx.standingPrompt)
|
|
@@ -2415,6 +2460,15 @@ async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG,
|
|
|
2415
2460
|
if (!ctx.credentialProxy) {
|
|
2416
2461
|
throw new Error("prepareCliTransport: ctx.credentialProxy is required — start a credential proxy " + "(see src/credentials) and pass { broker, proxyUrl }. There is no plaintext mode.");
|
|
2417
2462
|
}
|
|
2463
|
+
const capabilities = ctx.credentialProxy.capabilities;
|
|
2464
|
+
if (!Array.isArray(capabilities)) {
|
|
2465
|
+
throw new Error("prepareCliTransport: credentialProxy.capabilities is required " + "(empty array is allowed for zero-capability launches; undefined is a wiring bug)");
|
|
2466
|
+
}
|
|
2467
|
+
for (const c of capabilities) {
|
|
2468
|
+
if (typeof c !== "string" || c.includes(",")) {
|
|
2469
|
+
throw new Error(`prepareCliTransport: capability entry ${JSON.stringify(c)} contains a comma ` + `(each capability must be a single token; use ["send","read"] instead of ["send,read"])`);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2418
2472
|
ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
|
|
2419
2473
|
const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
|
|
2420
2474
|
const tokenFile = reg.voucherFile;
|
|
@@ -2515,17 +2569,28 @@ function firstExistingPath(candidates) {
|
|
|
2515
2569
|
}
|
|
2516
2570
|
return null;
|
|
2517
2571
|
}
|
|
2572
|
+
function looksLikeVersion(line) {
|
|
2573
|
+
return /\d+\.\d+/.test(line);
|
|
2574
|
+
}
|
|
2518
2575
|
function needsWindowsShimShell(command, platform) {
|
|
2519
2576
|
return platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
2520
2577
|
}
|
|
2521
2578
|
function probeCommandVersion(command, args = [], deps = {}, platform = process.platform) {
|
|
2522
2579
|
try {
|
|
2523
2580
|
const shell = needsWindowsShimShell(command, platform);
|
|
2524
|
-
const out = execFileSync(command, [...args, "--version"], {
|
|
2581
|
+
const out = execFileSync(command, [...args, "--version"], {
|
|
2582
|
+
encoding: "utf8",
|
|
2583
|
+
timeout: 5000,
|
|
2584
|
+
shell,
|
|
2585
|
+
input: "",
|
|
2586
|
+
env: { ...process.env, CI: "1" }
|
|
2587
|
+
});
|
|
2525
2588
|
const line = out.split(`
|
|
2526
2589
|
`)[0]?.trim();
|
|
2527
2590
|
if (!line)
|
|
2528
2591
|
return { ok: false, error: "empty_version_output" };
|
|
2592
|
+
if (!looksLikeVersion(line))
|
|
2593
|
+
return { ok: false, error: "invalid_version_output" };
|
|
2529
2594
|
return { ok: true, version: line };
|
|
2530
2595
|
} catch (err) {
|
|
2531
2596
|
const code = err?.code ?? err?.code ?? "version_probe_failed";
|
|
@@ -4062,6 +4127,36 @@ async function createDaemon(opts) {
|
|
|
4062
4127
|
}
|
|
4063
4128
|
});
|
|
4064
4129
|
const enrolledKeys = new Map;
|
|
4130
|
+
const typingTracker = createTypingScopeTracker();
|
|
4131
|
+
const typingHeartbeats = new Map;
|
|
4132
|
+
const TYPING_HEARTBEAT_MS = 5000;
|
|
4133
|
+
function stopTypingHeartbeat(agentId) {
|
|
4134
|
+
const timer = typingHeartbeats.get(agentId);
|
|
4135
|
+
if (timer) {
|
|
4136
|
+
clearInterval(timer);
|
|
4137
|
+
typingHeartbeats.delete(agentId);
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
function startTypingHeartbeat(agentId) {
|
|
4141
|
+
stopTypingHeartbeat(agentId);
|
|
4142
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4143
|
+
channel.reportAgentTyping?.({ agentId, dmConversationId });
|
|
4144
|
+
}
|
|
4145
|
+
const timer = setInterval(() => {
|
|
4146
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4147
|
+
channel.reportAgentTyping?.({ agentId, dmConversationId });
|
|
4148
|
+
}
|
|
4149
|
+
}, TYPING_HEARTBEAT_MS);
|
|
4150
|
+
timer.unref?.();
|
|
4151
|
+
typingHeartbeats.set(agentId, timer);
|
|
4152
|
+
}
|
|
4153
|
+
function emitTypingStopsAndClear(agentId) {
|
|
4154
|
+
stopTypingHeartbeat(agentId);
|
|
4155
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4156
|
+
channel.reportAgentTypingStop?.({ agentId, dmConversationId });
|
|
4157
|
+
}
|
|
4158
|
+
typingTracker.clear(agentId);
|
|
4159
|
+
}
|
|
4065
4160
|
const botsById = new Map;
|
|
4066
4161
|
async function listMyBotsHttp() {
|
|
4067
4162
|
const res = await fetch(`${opts.serverUrl}/api/community/daemon/bots`, {
|
|
@@ -4212,7 +4307,7 @@ async function createDaemon(opts) {
|
|
|
4212
4307
|
return {
|
|
4213
4308
|
agentId,
|
|
4214
4309
|
workingDirectory: workdirFor(agentId),
|
|
4215
|
-
credentialProxy: { broker, proxyUrl: proxy.url, runnerKey },
|
|
4310
|
+
credentialProxy: { broker, proxyUrl: proxy.url, runnerKey, capabilities: opts.capabilities },
|
|
4216
4311
|
agentCliPath: resolvedCliPath ?? opts.agentCliPath,
|
|
4217
4312
|
config: {
|
|
4218
4313
|
...botMeta?.name ? { agentName: botMeta.name } : {},
|
|
@@ -4224,7 +4319,16 @@ async function createDaemon(opts) {
|
|
|
4224
4319
|
},
|
|
4225
4320
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
4226
4321
|
onAgentSession: (info) => void channel.reportAgentSession(info),
|
|
4227
|
-
onAgentActivity: (info) =>
|
|
4322
|
+
onAgentActivity: (info) => {
|
|
4323
|
+
channel.reportAgentActivity?.(info);
|
|
4324
|
+
if (info.state === "starting" || info.state === "running") {
|
|
4325
|
+
if (!typingHeartbeats.has(info.agentId)) {
|
|
4326
|
+
startTypingHeartbeat(info.agentId);
|
|
4327
|
+
}
|
|
4328
|
+
} else {
|
|
4329
|
+
emitTypingStopsAndClear(info.agentId);
|
|
4330
|
+
}
|
|
4331
|
+
},
|
|
4228
4332
|
onBotAuditEvent: (agentId, event, context) => emitBotAuditEvent(agentId, event, context),
|
|
4229
4333
|
onAgentLocallyStopped: (info) => router?.markLocallyStopped(info.agentId),
|
|
4230
4334
|
sdkDriverDepsFor: (ctx) => createPiSdkDriverDeps(ctx),
|
|
@@ -4243,6 +4347,7 @@ async function createDaemon(opts) {
|
|
|
4243
4347
|
arch: opts.arch,
|
|
4244
4348
|
osRelease: opts.osRelease,
|
|
4245
4349
|
daemonVersion: opts.daemonVersion,
|
|
4350
|
+
typingTracker,
|
|
4246
4351
|
logger: log.child("router"),
|
|
4247
4352
|
onBeforeAgent: async (agentId) => {
|
|
4248
4353
|
if (!botsById.has(agentId)) {
|
|
@@ -4279,6 +4384,9 @@ async function createDaemon(opts) {
|
|
|
4279
4384
|
isOpen: () => channel.status === "open",
|
|
4280
4385
|
proxyUrl: proxy.url,
|
|
4281
4386
|
stop: async () => {
|
|
4387
|
+
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
4388
|
+
emitTypingStopsAndClear(agentId);
|
|
4389
|
+
}
|
|
4282
4390
|
channel.close();
|
|
4283
4391
|
await proxy.close();
|
|
4284
4392
|
await manager.stopAll();
|
|
@@ -4564,7 +4672,7 @@ async function daemonStart(opts) {
|
|
|
4564
4672
|
}
|
|
4565
4673
|
|
|
4566
4674
|
// ../shared/src/lib/invite-link.ts
|
|
4567
|
-
var INVITE_URL_RE = /(?:https?:\/\/[^\s/]+)?\/
|
|
4675
|
+
var INVITE_URL_RE = /(?:https?:\/\/[^\s/]+)?\/c\/invite\/([A-Za-z0-9_-]{6,64})/;
|
|
4568
4676
|
var BARE_TOKEN_RE = /^[A-Za-z0-9_-]{6,64}$/;
|
|
4569
4677
|
function parseInviteToken(input) {
|
|
4570
4678
|
const trimmed = input.trim();
|
package/dist/index.js
CHANGED
|
@@ -103,7 +103,7 @@ function serversSection() {
|
|
|
103
103
|
return [
|
|
104
104
|
"## Servers",
|
|
105
105
|
"",
|
|
106
|
-
`If a message contains a \`/
|
|
106
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces an owner-only check for you — it only accepts an invite your owner created, and " + "rejects anything else with a clear reason. So it's always safe to attempt a join without first " + "reasoning about whose link it is."
|
|
107
107
|
].join(`
|
|
108
108
|
`);
|
|
109
109
|
}
|
|
@@ -431,16 +431,6 @@ function writeAgentFile(workDir, systemPromptContent) {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
// src/drivers/cliTransport.ts
|
|
434
|
-
var DEFAULT_ACTIVE_CAPABILITIES = [
|
|
435
|
-
"send",
|
|
436
|
-
"read",
|
|
437
|
-
"mentions",
|
|
438
|
-
"tasks",
|
|
439
|
-
"reactions",
|
|
440
|
-
"server",
|
|
441
|
-
"channels",
|
|
442
|
-
"knowledge"
|
|
443
|
-
];
|
|
444
434
|
var DEFAULT_CLI_CONFIG = {
|
|
445
435
|
cliName: "alook",
|
|
446
436
|
envPrefix: "ALOOK",
|
|
@@ -452,7 +442,6 @@ function resolveStateHome(envPrefix) {
|
|
|
452
442
|
async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG, platform = process.platform) {
|
|
453
443
|
const E = cli.envPrefix;
|
|
454
444
|
const stateHome = resolveStateHome(E);
|
|
455
|
-
const capabilities = cli.activeCapabilities ?? DEFAULT_ACTIVE_CAPABILITIES;
|
|
456
445
|
const stateDir = path2.join(ctx.workingDirectory, cli.stateDirName);
|
|
457
446
|
await fs2.promises.mkdir(stateDir, { recursive: true });
|
|
458
447
|
if (ctx.standingPrompt)
|
|
@@ -461,6 +450,15 @@ async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG,
|
|
|
461
450
|
if (!ctx.credentialProxy) {
|
|
462
451
|
throw new Error("prepareCliTransport: ctx.credentialProxy is required — start a credential proxy " + "(see src/credentials) and pass { broker, proxyUrl }. There is no plaintext mode.");
|
|
463
452
|
}
|
|
453
|
+
const capabilities = ctx.credentialProxy.capabilities;
|
|
454
|
+
if (!Array.isArray(capabilities)) {
|
|
455
|
+
throw new Error("prepareCliTransport: credentialProxy.capabilities is required " + "(empty array is allowed for zero-capability launches; undefined is a wiring bug)");
|
|
456
|
+
}
|
|
457
|
+
for (const c of capabilities) {
|
|
458
|
+
if (typeof c !== "string" || c.includes(",")) {
|
|
459
|
+
throw new Error(`prepareCliTransport: capability entry ${JSON.stringify(c)} contains a comma ` + `(each capability must be a single token; use ["send","read"] instead of ["send,read"])`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
464
462
|
ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
|
|
465
463
|
const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
|
|
466
464
|
const tokenFile = reg.voucherFile;
|
|
@@ -561,17 +559,28 @@ function firstExistingPath(candidates) {
|
|
|
561
559
|
}
|
|
562
560
|
return null;
|
|
563
561
|
}
|
|
562
|
+
function looksLikeVersion(line) {
|
|
563
|
+
return /\d+\.\d+/.test(line);
|
|
564
|
+
}
|
|
564
565
|
function needsWindowsShimShell(command, platform) {
|
|
565
566
|
return platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
566
567
|
}
|
|
567
568
|
function probeCommandVersion(command, args = [], deps = {}, platform = process.platform) {
|
|
568
569
|
try {
|
|
569
570
|
const shell = needsWindowsShimShell(command, platform);
|
|
570
|
-
const out = execFileSync(command, [...args, "--version"], {
|
|
571
|
+
const out = execFileSync(command, [...args, "--version"], {
|
|
572
|
+
encoding: "utf8",
|
|
573
|
+
timeout: 5000,
|
|
574
|
+
shell,
|
|
575
|
+
input: "",
|
|
576
|
+
env: { ...process.env, CI: "1" }
|
|
577
|
+
});
|
|
571
578
|
const line = out.split(`
|
|
572
579
|
`)[0]?.trim();
|
|
573
580
|
if (!line)
|
|
574
581
|
return { ok: false, error: "empty_version_output" };
|
|
582
|
+
if (!looksLikeVersion(line))
|
|
583
|
+
return { ok: false, error: "invalid_version_output" };
|
|
575
584
|
return { ok: true, version: line };
|
|
576
585
|
} catch (err) {
|
|
577
586
|
const code = err?.code ?? err?.code ?? "version_probe_failed";
|
|
@@ -3905,6 +3914,8 @@ class AgentRouter {
|
|
|
3905
3914
|
latestSeq: cmd.unreadNotice.latestSeq
|
|
3906
3915
|
});
|
|
3907
3916
|
try {
|
|
3917
|
+
const beforeStatus = this.opts.manager.snapshot?.().agents?.[cmd.agentId]?.status ?? "unregistered";
|
|
3918
|
+
const wasActive = this.opts.typingTracker?.hasAny(cmd.agentId) ?? false;
|
|
3908
3919
|
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
3909
3920
|
this.opts.manager.register(cmd.agentId, {
|
|
3910
3921
|
runtimeConfig: cmd.config,
|
|
@@ -3912,8 +3923,17 @@ class AgentRouter {
|
|
|
3912
3923
|
launchId: cmd.launchId
|
|
3913
3924
|
});
|
|
3914
3925
|
this.running.add(cmd.agentId);
|
|
3926
|
+
const dmScope = cmd.unreadNotice.dmConversationId;
|
|
3927
|
+
if (dmScope)
|
|
3928
|
+
this.opts.typingTracker?.add(cmd.agentId, dmScope);
|
|
3915
3929
|
const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
|
|
3916
3930
|
this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
|
|
3931
|
+
if (dmScope && wasActive && beforeStatus === "running") {
|
|
3932
|
+
this.opts.channel.reportAgentTyping?.({
|
|
3933
|
+
agentId: cmd.agentId,
|
|
3934
|
+
dmConversationId: dmScope
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3917
3937
|
await this.opts.channel.reportWakeAck?.({
|
|
3918
3938
|
agentId: cmd.agentId,
|
|
3919
3939
|
launchId: cmd.launchId,
|
|
@@ -3994,6 +4014,31 @@ class AgentRouter {
|
|
|
3994
4014
|
}
|
|
3995
4015
|
}
|
|
3996
4016
|
}
|
|
4017
|
+
// src/manager/typingScopeTracker.ts
|
|
4018
|
+
function createTypingScopeTracker() {
|
|
4019
|
+
const scopes = new Map;
|
|
4020
|
+
return {
|
|
4021
|
+
add(agentId, dmConversationId) {
|
|
4022
|
+
let set = scopes.get(agentId);
|
|
4023
|
+
if (!set) {
|
|
4024
|
+
set = new Set;
|
|
4025
|
+
scopes.set(agentId, set);
|
|
4026
|
+
}
|
|
4027
|
+
set.add(dmConversationId);
|
|
4028
|
+
},
|
|
4029
|
+
snapshot(agentId) {
|
|
4030
|
+
const set = scopes.get(agentId);
|
|
4031
|
+
return set ? [...set] : [];
|
|
4032
|
+
},
|
|
4033
|
+
hasAny(agentId) {
|
|
4034
|
+
const set = scopes.get(agentId);
|
|
4035
|
+
return !!set && set.size > 0;
|
|
4036
|
+
},
|
|
4037
|
+
clear(agentId) {
|
|
4038
|
+
scopes.delete(agentId);
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
3997
4042
|
// src/credentials/credentialProxy.ts
|
|
3998
4043
|
import * as crypto from "crypto";
|
|
3999
4044
|
import * as fs5 from "fs";
|
|
@@ -4290,6 +4335,12 @@ class WsControlChannel {
|
|
|
4290
4335
|
async reportAgentActivity(info) {
|
|
4291
4336
|
this.sendFrame({ type: "agent_activity", ...info });
|
|
4292
4337
|
}
|
|
4338
|
+
reportAgentTyping(info) {
|
|
4339
|
+
this.sendFrame({ type: "agent_typing", ...info });
|
|
4340
|
+
}
|
|
4341
|
+
reportAgentTypingStop(info) {
|
|
4342
|
+
this.sendFrame({ type: "agent_typing_stop", ...info });
|
|
4343
|
+
}
|
|
4293
4344
|
async reportBotAuditEvent(frame) {
|
|
4294
4345
|
this.sendFrame(frame);
|
|
4295
4346
|
}
|
|
@@ -4848,6 +4899,36 @@ async function createDaemon(opts) {
|
|
|
4848
4899
|
}
|
|
4849
4900
|
});
|
|
4850
4901
|
const enrolledKeys = new Map;
|
|
4902
|
+
const typingTracker = createTypingScopeTracker();
|
|
4903
|
+
const typingHeartbeats = new Map;
|
|
4904
|
+
const TYPING_HEARTBEAT_MS = 5000;
|
|
4905
|
+
function stopTypingHeartbeat(agentId) {
|
|
4906
|
+
const timer = typingHeartbeats.get(agentId);
|
|
4907
|
+
if (timer) {
|
|
4908
|
+
clearInterval(timer);
|
|
4909
|
+
typingHeartbeats.delete(agentId);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
function startTypingHeartbeat(agentId) {
|
|
4913
|
+
stopTypingHeartbeat(agentId);
|
|
4914
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4915
|
+
channel.reportAgentTyping?.({ agentId, dmConversationId });
|
|
4916
|
+
}
|
|
4917
|
+
const timer = setInterval(() => {
|
|
4918
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4919
|
+
channel.reportAgentTyping?.({ agentId, dmConversationId });
|
|
4920
|
+
}
|
|
4921
|
+
}, TYPING_HEARTBEAT_MS);
|
|
4922
|
+
timer.unref?.();
|
|
4923
|
+
typingHeartbeats.set(agentId, timer);
|
|
4924
|
+
}
|
|
4925
|
+
function emitTypingStopsAndClear(agentId) {
|
|
4926
|
+
stopTypingHeartbeat(agentId);
|
|
4927
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4928
|
+
channel.reportAgentTypingStop?.({ agentId, dmConversationId });
|
|
4929
|
+
}
|
|
4930
|
+
typingTracker.clear(agentId);
|
|
4931
|
+
}
|
|
4851
4932
|
const botsById = new Map;
|
|
4852
4933
|
async function listMyBotsHttp() {
|
|
4853
4934
|
const res = await fetch(`${opts.serverUrl}/api/community/daemon/bots`, {
|
|
@@ -4998,7 +5079,7 @@ async function createDaemon(opts) {
|
|
|
4998
5079
|
return {
|
|
4999
5080
|
agentId,
|
|
5000
5081
|
workingDirectory: workdirFor(agentId),
|
|
5001
|
-
credentialProxy: { broker, proxyUrl: proxy.url, runnerKey },
|
|
5082
|
+
credentialProxy: { broker, proxyUrl: proxy.url, runnerKey, capabilities: opts.capabilities },
|
|
5002
5083
|
agentCliPath: resolvedCliPath ?? opts.agentCliPath,
|
|
5003
5084
|
config: {
|
|
5004
5085
|
...botMeta?.name ? { agentName: botMeta.name } : {},
|
|
@@ -5010,7 +5091,16 @@ async function createDaemon(opts) {
|
|
|
5010
5091
|
},
|
|
5011
5092
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
5012
5093
|
onAgentSession: (info) => void channel.reportAgentSession(info),
|
|
5013
|
-
onAgentActivity: (info) =>
|
|
5094
|
+
onAgentActivity: (info) => {
|
|
5095
|
+
channel.reportAgentActivity?.(info);
|
|
5096
|
+
if (info.state === "starting" || info.state === "running") {
|
|
5097
|
+
if (!typingHeartbeats.has(info.agentId)) {
|
|
5098
|
+
startTypingHeartbeat(info.agentId);
|
|
5099
|
+
}
|
|
5100
|
+
} else {
|
|
5101
|
+
emitTypingStopsAndClear(info.agentId);
|
|
5102
|
+
}
|
|
5103
|
+
},
|
|
5014
5104
|
onBotAuditEvent: (agentId, event, context) => emitBotAuditEvent(agentId, event, context),
|
|
5015
5105
|
onAgentLocallyStopped: (info) => router?.markLocallyStopped(info.agentId),
|
|
5016
5106
|
sdkDriverDepsFor: (ctx) => createPiSdkDriverDeps(ctx),
|
|
@@ -5029,6 +5119,7 @@ async function createDaemon(opts) {
|
|
|
5029
5119
|
arch: opts.arch,
|
|
5030
5120
|
osRelease: opts.osRelease,
|
|
5031
5121
|
daemonVersion: opts.daemonVersion,
|
|
5122
|
+
typingTracker,
|
|
5032
5123
|
logger: log.child("router"),
|
|
5033
5124
|
onBeforeAgent: async (agentId) => {
|
|
5034
5125
|
if (!botsById.has(agentId)) {
|
|
@@ -5065,6 +5156,9 @@ async function createDaemon(opts) {
|
|
|
5065
5156
|
isOpen: () => channel.status === "open",
|
|
5066
5157
|
proxyUrl: proxy.url,
|
|
5067
5158
|
stop: async () => {
|
|
5159
|
+
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
5160
|
+
emitTypingStopsAndClear(agentId);
|
|
5161
|
+
}
|
|
5068
5162
|
channel.close();
|
|
5069
5163
|
await proxy.close();
|
|
5070
5164
|
await manager.stopAll();
|
|
@@ -5111,6 +5205,7 @@ export {
|
|
|
5111
5205
|
descriptorFromDriver,
|
|
5112
5206
|
deriveCliFallbackCandidates,
|
|
5113
5207
|
deriveAuditLogSubcommand,
|
|
5208
|
+
createTypingScopeTracker,
|
|
5114
5209
|
createInitialManagerState,
|
|
5115
5210
|
createInitialApmGatedSteeringState,
|
|
5116
5211
|
createDaemon,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alook/daemon",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.157",
|
|
4
4
|
"description": "Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/alookai/alook#readme",
|