@alook/daemon 0.0.155 → 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 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
- const json = await res.json();
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
- const json = await res.json();
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
- const body = await res.json().catch(() => ({}));
71
- const e = new Error(body?.error ?? `proxy api/attachmentDownload failed (${res.status})`);
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 \`/community/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."
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"], { encoding: "utf8", timeout: 5000, shell });
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) => void channel.reportAgentActivity?.(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/]+)?\/community\/invite\/([A-Za-z0-9_-]{6,64})/;
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 \`/community/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."
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"], { encoding: "utf8", timeout: 5000, shell });
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) => void channel.reportAgentActivity?.(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.155",
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",