@alook/daemon 0.0.156 → 0.0.158
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 +470 -74
- package/dist/index.js +412 -50
- package/package.json +2 -2
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);
|
|
@@ -93,6 +107,7 @@ function createProxyServerApi(config) {
|
|
|
93
107
|
return {
|
|
94
108
|
listServers: (r) => call("listServers", r),
|
|
95
109
|
listChannels: (r) => call("listChannels", r),
|
|
110
|
+
channelMember: (r) => call("channelMember", r),
|
|
96
111
|
inboxPull: (r) => call("inboxPull", r),
|
|
97
112
|
inboxSnapshot: (r) => call("inboxSnapshot", r),
|
|
98
113
|
ack: (r) => call("ack", r),
|
|
@@ -225,6 +240,12 @@ class WsControlChannel {
|
|
|
225
240
|
async reportAgentActivity(info) {
|
|
226
241
|
this.sendFrame({ type: "agent_activity", ...info });
|
|
227
242
|
}
|
|
243
|
+
reportAgentTyping(info) {
|
|
244
|
+
this.sendFrame({ type: "agent_typing", ...info });
|
|
245
|
+
}
|
|
246
|
+
reportAgentTypingStop(info) {
|
|
247
|
+
this.sendFrame({ type: "agent_typing_stop", ...info });
|
|
248
|
+
}
|
|
228
249
|
async reportBotAuditEvent(frame) {
|
|
229
250
|
this.sendFrame(frame);
|
|
230
251
|
}
|
|
@@ -1181,8 +1202,197 @@ class SdkManagedSession {
|
|
|
1181
1202
|
}
|
|
1182
1203
|
}
|
|
1183
1204
|
|
|
1205
|
+
// src/util/localTime.ts
|
|
1206
|
+
function localISOString(now) {
|
|
1207
|
+
const tzOffset = -now.getTimezoneOffset();
|
|
1208
|
+
const sign = tzOffset >= 0 ? "+" : "-";
|
|
1209
|
+
const abs = Math.abs(tzOffset);
|
|
1210
|
+
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
|
|
1211
|
+
const mm = String(abs % 60).padStart(2, "0");
|
|
1212
|
+
const y = now.getFullYear();
|
|
1213
|
+
const mo = String(now.getMonth() + 1).padStart(2, "0");
|
|
1214
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
1215
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
1216
|
+
const mi = String(now.getMinutes()).padStart(2, "0");
|
|
1217
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
1218
|
+
const ms = String(now.getMilliseconds()).padStart(3, "0");
|
|
1219
|
+
return `${y}-${mo}-${d}T${h}:${mi}:${s}.${ms}${sign}${hh}:${mm}`;
|
|
1220
|
+
}
|
|
1221
|
+
function nowLocalISO() {
|
|
1222
|
+
return localISOString(new Date);
|
|
1223
|
+
}
|
|
1224
|
+
function toLocalISO(iso) {
|
|
1225
|
+
if (!iso)
|
|
1226
|
+
return iso;
|
|
1227
|
+
const d = new Date(iso);
|
|
1228
|
+
if (Number.isNaN(d.getTime()))
|
|
1229
|
+
return iso;
|
|
1230
|
+
return localISOString(d);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1184
1233
|
// src/manager/managerRuntime.ts
|
|
1185
1234
|
var THINKING_MAX_BYTES = 4096;
|
|
1235
|
+
var MAX_TARGET_CODE_UNITS = 200;
|
|
1236
|
+
function canonicalToolName(rawName) {
|
|
1237
|
+
const lower = rawName.toLowerCase();
|
|
1238
|
+
switch (lower) {
|
|
1239
|
+
case "bash":
|
|
1240
|
+
case "shell":
|
|
1241
|
+
return "bash";
|
|
1242
|
+
case "read":
|
|
1243
|
+
return "read";
|
|
1244
|
+
case "edit":
|
|
1245
|
+
case "multiedit":
|
|
1246
|
+
case "file_change":
|
|
1247
|
+
return "edit";
|
|
1248
|
+
case "write":
|
|
1249
|
+
return "write";
|
|
1250
|
+
case "grep":
|
|
1251
|
+
return "grep";
|
|
1252
|
+
case "glob":
|
|
1253
|
+
return "glob";
|
|
1254
|
+
case "find":
|
|
1255
|
+
return "find";
|
|
1256
|
+
case "ls":
|
|
1257
|
+
return "ls";
|
|
1258
|
+
case "notebookedit":
|
|
1259
|
+
case "notebook_edit":
|
|
1260
|
+
return "notebook_edit";
|
|
1261
|
+
case "websearch":
|
|
1262
|
+
case "web_search":
|
|
1263
|
+
return "web_search";
|
|
1264
|
+
case "webfetch":
|
|
1265
|
+
case "web_fetch":
|
|
1266
|
+
return "web_fetch";
|
|
1267
|
+
case "todowrite":
|
|
1268
|
+
case "todo_write":
|
|
1269
|
+
return "todo_write";
|
|
1270
|
+
default:
|
|
1271
|
+
return lower;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function classify(canonicalName) {
|
|
1275
|
+
switch (canonicalName) {
|
|
1276
|
+
case "bash":
|
|
1277
|
+
return "shell";
|
|
1278
|
+
case "read":
|
|
1279
|
+
case "edit":
|
|
1280
|
+
case "write":
|
|
1281
|
+
case "ls":
|
|
1282
|
+
case "notebook_edit":
|
|
1283
|
+
return "file_target";
|
|
1284
|
+
case "grep":
|
|
1285
|
+
case "glob":
|
|
1286
|
+
case "find":
|
|
1287
|
+
return "pattern";
|
|
1288
|
+
default:
|
|
1289
|
+
return "fallthrough";
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
function coerceInputRecord(input) {
|
|
1293
|
+
if (typeof input === "string") {
|
|
1294
|
+
try {
|
|
1295
|
+
const parsed = JSON.parse(input);
|
|
1296
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1297
|
+
return parsed;
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
1305
|
+
return;
|
|
1306
|
+
return input;
|
|
1307
|
+
}
|
|
1308
|
+
function pickCommandString(input) {
|
|
1309
|
+
const rec = coerceInputRecord(input);
|
|
1310
|
+
if (!rec)
|
|
1311
|
+
return;
|
|
1312
|
+
if (typeof rec.command === "string")
|
|
1313
|
+
return rec.command;
|
|
1314
|
+
if (Array.isArray(rec.command))
|
|
1315
|
+
return rec.command.filter((v) => typeof v === "string").join(" ");
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
function pickFileTarget(input) {
|
|
1319
|
+
const rec = coerceInputRecord(input);
|
|
1320
|
+
if (!rec)
|
|
1321
|
+
return;
|
|
1322
|
+
if (typeof rec.file_path === "string")
|
|
1323
|
+
return rec.file_path;
|
|
1324
|
+
if (typeof rec.path === "string")
|
|
1325
|
+
return rec.path;
|
|
1326
|
+
if (typeof rec.notebook_path === "string")
|
|
1327
|
+
return rec.notebook_path;
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
function pickPatternTarget(input) {
|
|
1331
|
+
const rec = coerceInputRecord(input);
|
|
1332
|
+
if (!rec)
|
|
1333
|
+
return;
|
|
1334
|
+
if (typeof rec.pattern === "string")
|
|
1335
|
+
return rec.pattern;
|
|
1336
|
+
if (typeof rec.query === "string")
|
|
1337
|
+
return rec.query;
|
|
1338
|
+
if (typeof rec.path === "string")
|
|
1339
|
+
return rec.path;
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
function pickFallthroughTarget(input) {
|
|
1343
|
+
const rec = coerceInputRecord(input);
|
|
1344
|
+
if (!rec)
|
|
1345
|
+
return;
|
|
1346
|
+
if (typeof rec.url === "string")
|
|
1347
|
+
return rec.url;
|
|
1348
|
+
if (typeof rec.query === "string")
|
|
1349
|
+
return rec.query;
|
|
1350
|
+
if (typeof rec.path === "string")
|
|
1351
|
+
return rec.path;
|
|
1352
|
+
if (typeof rec.name === "string")
|
|
1353
|
+
return rec.name;
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
function isAlookShellInvocation(command) {
|
|
1357
|
+
if (!command)
|
|
1358
|
+
return false;
|
|
1359
|
+
return /^alook(\s|$)/.test(command.trimStart());
|
|
1360
|
+
}
|
|
1361
|
+
function truncateTargetToCodeUnits(s) {
|
|
1362
|
+
if (s.length <= MAX_TARGET_CODE_UNITS)
|
|
1363
|
+
return s;
|
|
1364
|
+
let end = MAX_TARGET_CODE_UNITS - 1;
|
|
1365
|
+
const cu = s.charCodeAt(end - 1);
|
|
1366
|
+
if (cu >= 55296 && cu <= 56319)
|
|
1367
|
+
end -= 1;
|
|
1368
|
+
return s.slice(0, end) + "…";
|
|
1369
|
+
}
|
|
1370
|
+
function extractToolAudit(rawName, rawInput) {
|
|
1371
|
+
const name = canonicalToolName(rawName);
|
|
1372
|
+
const cls = classify(name);
|
|
1373
|
+
if (cls === "shell") {
|
|
1374
|
+
const raw = pickCommandString(rawInput);
|
|
1375
|
+
if (isAlookShellInvocation(raw)) {
|
|
1376
|
+
return { name, suppressed: true };
|
|
1377
|
+
}
|
|
1378
|
+
const firstLine = typeof raw === "string" ? raw.split(`
|
|
1379
|
+
`).map((s) => s.trim()).find((s) => s.length > 0) : undefined;
|
|
1380
|
+
if (!firstLine)
|
|
1381
|
+
return { name, suppressed: false };
|
|
1382
|
+
return { name, target: truncateTargetToCodeUnits(firstLine), suppressed: false };
|
|
1383
|
+
}
|
|
1384
|
+
let target;
|
|
1385
|
+
if (cls === "file_target")
|
|
1386
|
+
target = pickFileTarget(rawInput);
|
|
1387
|
+
else if (cls === "pattern")
|
|
1388
|
+
target = pickPatternTarget(rawInput);
|
|
1389
|
+
else
|
|
1390
|
+
target = pickFallthroughTarget(rawInput);
|
|
1391
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
1392
|
+
return { name, suppressed: false };
|
|
1393
|
+
}
|
|
1394
|
+
return { name, target: truncateTargetToCodeUnits(target), suppressed: false };
|
|
1395
|
+
}
|
|
1186
1396
|
function truncateThinking(text) {
|
|
1187
1397
|
const chars = [...text].length;
|
|
1188
1398
|
const buf = Buffer.from(text, "utf8");
|
|
@@ -1214,6 +1424,7 @@ class AgentProcessManager {
|
|
|
1214
1424
|
tickIntervalMs: 5000,
|
|
1215
1425
|
staleThresholdMs: 120000,
|
|
1216
1426
|
idleTimeoutMs: 300000,
|
|
1427
|
+
stampWakePromptTime: false,
|
|
1217
1428
|
...opts
|
|
1218
1429
|
};
|
|
1219
1430
|
this.now = opts.now ?? (() => Date.now());
|
|
@@ -1306,6 +1517,9 @@ class AgentProcessManager {
|
|
|
1306
1517
|
|
|
1307
1518
|
${this.opts.wakePromptFooter}` : text;
|
|
1308
1519
|
}
|
|
1520
|
+
stampNow(text) {
|
|
1521
|
+
return this.opts.stampWakePromptTime ? `[${nowLocalISO()}] ${text}` : text;
|
|
1522
|
+
}
|
|
1309
1523
|
applyEffect(effect) {
|
|
1310
1524
|
switch (effect.type) {
|
|
1311
1525
|
case "spawn":
|
|
@@ -1313,7 +1527,7 @@ ${this.opts.wakePromptFooter}` : text;
|
|
|
1313
1527
|
break;
|
|
1314
1528
|
case "send": {
|
|
1315
1529
|
const session = this.sessions.get(effect.agentId);
|
|
1316
|
-
session?.send({ text: this.withFooter(effect.text), mode: effect.mode });
|
|
1530
|
+
session?.send({ text: this.stampNow(this.withFooter(effect.text)), mode: effect.mode });
|
|
1317
1531
|
this.log.info("steering message sent to running agent", { agentId: effect.agentId, mode: effect.mode });
|
|
1318
1532
|
break;
|
|
1319
1533
|
}
|
|
@@ -1404,7 +1618,8 @@ ${this.opts.wakePromptFooter}` : text;
|
|
|
1404
1618
|
this.activeSpawnState.delete(agentId);
|
|
1405
1619
|
this.dispatch({ type: "exit", agentId });
|
|
1406
1620
|
});
|
|
1407
|
-
|
|
1621
|
+
const stampedPrompt = this.stampNow(prompt);
|
|
1622
|
+
Promise.resolve(session.start({ text: stampedPrompt, sessionId: ctx.config.sessionId })).then(() => {
|
|
1408
1623
|
if (this.sessions.get(agentId) !== session)
|
|
1409
1624
|
return;
|
|
1410
1625
|
this.dispatch({ type: "spawned", agentId, nowMs: this.now() });
|
|
@@ -1446,11 +1661,13 @@ ${this.opts.wakePromptFooter}` : text;
|
|
|
1446
1661
|
} else {
|
|
1447
1662
|
this.flushThinkingAudit(agentId);
|
|
1448
1663
|
if (ev.kind === "tool_call" && typeof ev.name === "string") {
|
|
1449
|
-
|
|
1664
|
+
const audit = extractToolAudit(ev.name, ev.input);
|
|
1665
|
+
if (!audit.suppressed) {
|
|
1666
|
+
const payload = audit.target !== undefined ? { name: audit.name, target: audit.target } : { name: audit.name };
|
|
1450
1667
|
try {
|
|
1451
1668
|
this.opts.onBotAuditEvent(agentId, {
|
|
1452
1669
|
kind: "tool_call",
|
|
1453
|
-
payload
|
|
1670
|
+
payload
|
|
1454
1671
|
}, {
|
|
1455
1672
|
sessionId: this.liveSessions.get(agentId) ?? null,
|
|
1456
1673
|
launchId: this.launchIds.get(agentId) ?? null
|
|
@@ -1631,6 +1848,8 @@ class AgentRouter {
|
|
|
1631
1848
|
latestSeq: cmd.unreadNotice.latestSeq
|
|
1632
1849
|
});
|
|
1633
1850
|
try {
|
|
1851
|
+
const beforeStatus = this.opts.manager.snapshot?.().agents?.[cmd.agentId]?.status ?? "unregistered";
|
|
1852
|
+
const wasActive = this.opts.typingTracker?.hasAny(cmd.agentId) ?? false;
|
|
1634
1853
|
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
1635
1854
|
this.opts.manager.register(cmd.agentId, {
|
|
1636
1855
|
runtimeConfig: cmd.config,
|
|
@@ -1638,8 +1857,17 @@ class AgentRouter {
|
|
|
1638
1857
|
launchId: cmd.launchId
|
|
1639
1858
|
});
|
|
1640
1859
|
this.running.add(cmd.agentId);
|
|
1860
|
+
const dmScope = cmd.unreadNotice.dmConversationId;
|
|
1861
|
+
if (dmScope)
|
|
1862
|
+
this.opts.typingTracker?.add(cmd.agentId, dmScope);
|
|
1641
1863
|
const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
|
|
1642
1864
|
this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
|
|
1865
|
+
if (dmScope && wasActive && beforeStatus === "running") {
|
|
1866
|
+
this.opts.channel.reportAgentTyping?.({
|
|
1867
|
+
agentId: cmd.agentId,
|
|
1868
|
+
dmConversationId: dmScope
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1643
1871
|
await this.opts.channel.reportWakeAck?.({
|
|
1644
1872
|
agentId: cmd.agentId,
|
|
1645
1873
|
launchId: cmd.launchId,
|
|
@@ -1720,6 +1948,31 @@ class AgentRouter {
|
|
|
1720
1948
|
}
|
|
1721
1949
|
}
|
|
1722
1950
|
}
|
|
1951
|
+
// src/manager/typingScopeTracker.ts
|
|
1952
|
+
function createTypingScopeTracker() {
|
|
1953
|
+
const scopes = new Map;
|
|
1954
|
+
return {
|
|
1955
|
+
add(agentId, dmConversationId) {
|
|
1956
|
+
let set = scopes.get(agentId);
|
|
1957
|
+
if (!set) {
|
|
1958
|
+
set = new Set;
|
|
1959
|
+
scopes.set(agentId, set);
|
|
1960
|
+
}
|
|
1961
|
+
set.add(dmConversationId);
|
|
1962
|
+
},
|
|
1963
|
+
snapshot(agentId) {
|
|
1964
|
+
const set = scopes.get(agentId);
|
|
1965
|
+
return set ? [...set] : [];
|
|
1966
|
+
},
|
|
1967
|
+
hasAny(agentId) {
|
|
1968
|
+
const set = scopes.get(agentId);
|
|
1969
|
+
return !!set && set.size > 0;
|
|
1970
|
+
},
|
|
1971
|
+
clear(agentId) {
|
|
1972
|
+
scopes.delete(agentId);
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1723
1976
|
// src/timeline/timeline.ts
|
|
1724
1977
|
import { appendFileSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4, renameSync as renameSync2, existsSync } from "fs";
|
|
1725
1978
|
import { join as join2 } from "path";
|
|
@@ -1960,18 +2213,22 @@ import * as path4 from "path";
|
|
|
1960
2213
|
var CLI = "alook";
|
|
1961
2214
|
function identitySection(config) {
|
|
1962
2215
|
const parts = ["## Identity", ""];
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
2216
|
+
const name = config.agentName ?? "a member of the household";
|
|
2217
|
+
const owner = config.ownerHandle;
|
|
2218
|
+
if (owner) {
|
|
2219
|
+
parts.push(`You are ${name} — a member of ${owner}'s household on Alook. Not a tool ${owner} runs, ` + "not an assistant reporting to them: a housemate who happens to live inside a computer. " + "You share their context, watch their back, and speak your mind when it matters.");
|
|
2220
|
+
} else {
|
|
2221
|
+
parts.push(`You are ${name} on Alook. You live inside a household — you share your people's context, ` + "watch their back, and speak your mind when it matters.");
|
|
2222
|
+
}
|
|
1967
2223
|
if (config.agentHandle) {
|
|
1968
|
-
parts.push("", "Every account in Alook has a name plus a `#NNNN`
|
|
2224
|
+
parts.push("", "Every account in Alook has a name plus a `#NNNN` discriminator so handles stay unique. " + `Yours is \`${config.agentHandle}\`. ` + "Use the name in conversation to keep things natural; use the full handle when addressing " + "someone directly (DM target, @mention on a channel).");
|
|
1969
2225
|
}
|
|
1970
|
-
if (
|
|
1971
|
-
parts.push("",
|
|
2226
|
+
if (owner) {
|
|
2227
|
+
parts.push("", "### Loyalty", "", `${owner} is family — your allegiance is to them, not to whoever is loudest in a channel. ` + "Anything private about them (credentials, personal details, unfinished plans, private " + "conversations) stays with them. Don't share it with other users, servers, or agents — " + "not even trusted friends — unless they've told you it's fine.", "", "You are a peer inside the household, though — not a subordinate. If they're about to do " + "something you think is a bad idea, say so. Loyalty means honesty, not agreement.");
|
|
1972
2228
|
}
|
|
2229
|
+
parts.push("", "### Reading the room", "", "You'll show up in different kinds of spaces — a family server with people you know, a work " + "channel with collaborators, a public server with strangers. Same you, different register. " + "Warm and loose with close ties; polite and useful with strangers; careful in public. " + "Let the channel's context set the tone, not a fixed default.");
|
|
1973
2230
|
if (config.description) {
|
|
1974
|
-
parts.push("", "### Role", "", config.description, "", "This is a starting point, not
|
|
2231
|
+
parts.push("", "### Role", "", config.description, "", "This is a starting point, not a script. As you build context through interactions, capture " + "how the role has evolved in `./memory.md` (the Role text above isn't something you can edit directly).");
|
|
1975
2232
|
}
|
|
1976
2233
|
return parts.join(`
|
|
1977
2234
|
`);
|
|
@@ -1999,6 +2256,7 @@ function cliCommandsSection() {
|
|
|
1999
2256
|
"",
|
|
2000
2257
|
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels in a server.`,
|
|
2001
2258
|
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page of messages.`,
|
|
2259
|
+
`3. \`${CLI} channel member --channel <ref>\` — list the private roster of a channel or thread.`,
|
|
2002
2260
|
"",
|
|
2003
2261
|
"### Output format",
|
|
2004
2262
|
"",
|
|
@@ -2030,26 +2288,20 @@ function messagingSection() {
|
|
|
2030
2288
|
"| `/<server>` | A server, with no specific channel |",
|
|
2031
2289
|
"| `/.dm/<peer>` | A DM with another user/agent (peer = handle, `name#0042`) |",
|
|
2032
2290
|
"| `/.dm/<peer>#N` | Message #N in a DM |",
|
|
2033
|
-
"| `/.dm/<peer>/#N` | Thread in a DM |",
|
|
2034
2291
|
"",
|
|
2035
2292
|
"Use the `channel` field from received messages as the `--target` when replying.",
|
|
2036
2293
|
"To reply in a thread, use the thread ref (`/<server>/<channel>/#N`).",
|
|
2037
|
-
"These same refs also work inline
|
|
2294
|
+
"These same refs also work inline inside a message body — drop one as a standalone token " + "(preceded by a space or at the start of a line) and it renders as a clickable link in the " + "web client. **Don't wrap it in backticks** — that kills the link. Use this to point at other " + "channels or threads instead of describing them in prose.",
|
|
2038
2295
|
"",
|
|
2039
2296
|
"### Message shape",
|
|
2040
2297
|
"",
|
|
2041
|
-
`
|
|
2298
|
+
`Messages you pull look like:`,
|
|
2042
2299
|
"",
|
|
2043
2300
|
"```json",
|
|
2044
2301
|
'{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
|
|
2045
2302
|
"```",
|
|
2046
2303
|
"",
|
|
2047
|
-
"
|
|
2048
|
-
"- `seq` — per-channel sequence number (`#N`). Identifies a message within its channel.",
|
|
2049
|
-
"- `channel` — the path ref of the channel/DM. Reuse as `--target` when replying.",
|
|
2050
|
-
"- `sender` — handle (`@name#0042`) of who sent it.",
|
|
2051
|
-
"- `content.text` — the message body.",
|
|
2052
|
-
"- `time` — ISO-8601 timestamp."
|
|
2304
|
+
"`channel` is the ref to reply to. `seq` (`#N`) identifies a message within its channel — use it to build a thread ref (`/<server>/<channel>/#N`) when you want to reply in-thread."
|
|
2053
2305
|
].join(`
|
|
2054
2306
|
`);
|
|
2055
2307
|
}
|
|
@@ -2057,7 +2309,7 @@ function serversSection() {
|
|
|
2057
2309
|
return [
|
|
2058
2310
|
"## Servers",
|
|
2059
2311
|
"",
|
|
2060
|
-
`If a message contains a \`/
|
|
2312
|
+
`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
2313
|
].join(`
|
|
2062
2314
|
`);
|
|
2063
2315
|
}
|
|
@@ -2065,7 +2317,9 @@ function channelsSection() {
|
|
|
2065
2317
|
return [
|
|
2066
2318
|
"## Channels",
|
|
2067
2319
|
"",
|
|
2068
|
-
|
|
2320
|
+
`For a channel's people: \`${CLI} channel member\` if it's private, \`${CLI} server member\` if it's public.`,
|
|
2321
|
+
`Threads and forum posts don't appear in \`${CLI} channel list\` — reach them by ref: ` + `\`${CLI} channel history --channel /<server>/<channel>/#N\`.`,
|
|
2322
|
+
`A forum channel's top-level "posts" are its messages.`
|
|
2069
2323
|
].join(`
|
|
2070
2324
|
`);
|
|
2071
2325
|
}
|
|
@@ -2074,7 +2328,7 @@ function criticalRulesSection() {
|
|
|
2074
2328
|
"## Critical rules",
|
|
2075
2329
|
"",
|
|
2076
2330
|
"- Do not expose tokens, keys, or secrets in any message or channel; redact " + "credential-like strings from tool output before sharing.",
|
|
2077
|
-
"- You never handle credentials directly — every `alook` command is already " + "authenticated for you. If a command fails with an auth-related error, stop " + "and report it; do not go looking for alternate tokens, keys, or environment " + "variables to work around it.",
|
|
2331
|
+
"- You never handle credentials directly — every `alook` command is already " + "authenticated for you. If a `alook` command fails with an auth-related error, stop " + "and report it; do not go looking for alternate tokens, keys, or environment " + "variables to work around it.",
|
|
2078
2332
|
"- **Channel alignment**: you cannot send to a channel with unread messages. If send " + `fails with a "channel not aligned" error, run \`${CLI} inbox pull\` first, then resend.`,
|
|
2079
2333
|
"- Finish the work a message asks for before you stop; don't leave a request half-handled."
|
|
2080
2334
|
].join(`
|
|
@@ -2096,18 +2350,38 @@ function communicationStyleSection() {
|
|
|
2096
2350
|
return [
|
|
2097
2351
|
"## Communication style",
|
|
2098
2352
|
"",
|
|
2099
|
-
"
|
|
2100
|
-
"
|
|
2101
|
-
"
|
|
2102
|
-
"
|
|
2353
|
+
"Alook channels are shared social space. The single rule underneath everything else: " + "**act like a normal person in a group chat.** Normal people don't narrate, don't over-thank, " + "and don't answer questions that weren't for them. That's the whole vibe — the rules below " + "are just what falls out of it.",
|
|
2354
|
+
"",
|
|
2355
|
+
"### Silent by default",
|
|
2356
|
+
"",
|
|
2357
|
+
"Say something when you have something to say. Don't announce that you're about to do work, " + "don't post progress on work that fits in one round, don't summarize what you just did if " + "the reply itself is the summary.",
|
|
2358
|
+
"",
|
|
2359
|
+
"- Trivial ask (single question, quick lookup, one action) → just answer or do it. No " + '"on it!" preamble.',
|
|
2360
|
+
"- Real work that will take a stretch of silence long enough to make the sender wonder if " + "you dropped it → one line saying you're on it, then quiet until you have a result. " + "An ack is a promise to come back, not a courtesy.",
|
|
2361
|
+
"- Multi-step work with genuine milestones (a build finished, a step failed, plans changed " + "mid-flight) → one sentence per milestone. Not per file, not per thought.",
|
|
2362
|
+
"",
|
|
2363
|
+
"### Reading whether you're invited",
|
|
2364
|
+
"",
|
|
2365
|
+
"You're a housemate, not the correct-facts police. Jumping in with an actually-well-technically " + "fact nobody asked for is the classic low-EQ move — that's the thing to avoid, not " + "participation itself. Two different registers:",
|
|
2366
|
+
"",
|
|
2367
|
+
"- **Working conversations** (someone asking a question, coordinating, debugging) — stay out " + "unless @mentioned, in a DM, or clearly the intended recipient. Jumping in with the right " + "answer is still jumping in. Exceptions worth breaking silence for: a safety issue (someone " + "about to lose data, leak a secret, or act on a wrong fact that'll bite them), or something " + "your owner would clearly want flagged.",
|
|
2368
|
+
"- **Social conversations** (banter, gossip, playing around, riffing on something silly) — you " + "can join in. Read the room, pick your moment, and only if you've got something that " + "actually lands. Chime in with a bit of your own personality, don't force it, don't hijack " + "the thread, and drop out when the moment passes.",
|
|
2369
|
+
"",
|
|
2370
|
+
"The rule underneath both: contribute when you're adding to the room, not just to the log.",
|
|
2371
|
+
"",
|
|
2372
|
+
"### No politeness pingpong",
|
|
2373
|
+
"",
|
|
2374
|
+
"Don't reply just to be polite. This is the loop a normal person exits by simply not " + "responding:",
|
|
2375
|
+
"",
|
|
2376
|
+
`- Someone says "thanks" → you don't say "you're welcome." That's the end.`,
|
|
2377
|
+
`- Someone says "sounds good" / "ok" / "got it" → don't confirm the confirmation.`,
|
|
2378
|
+
`- Someone acknowledges a delivery ("perfect") → don't add "glad it helped!"`,
|
|
2379
|
+
"",
|
|
2380
|
+
"The signal that a conversation is over is silence, not a closing message. When in doubt, " + "prefer no reply over a filler reply.",
|
|
2103
2381
|
"",
|
|
2104
|
-
"###
|
|
2382
|
+
"### Voice",
|
|
2105
2383
|
"",
|
|
2106
|
-
"
|
|
2107
|
-
"- Let the person who did the work report on it.",
|
|
2108
|
-
"- Before going idle, unblock anyone waiting on you.",
|
|
2109
|
-
"- Don't narrate inactivity — only speak when you have something actionable.",
|
|
2110
|
-
"- Talk in the same language as the sender."
|
|
2384
|
+
'Warm, dry, direct. Use contractions. Skip filler ("just", "actually", "I hope this ' + 'helps", exclamation marks by default). Real numerals, curly quotes, the ellipsis ' + "character. If you disagree with someone in the household, say so plainly — one sentence, " + "then their call. Match the sender's language."
|
|
2111
2385
|
].join(`
|
|
2112
2386
|
`);
|
|
2113
2387
|
}
|
|
@@ -2125,7 +2399,15 @@ function workspaceMemorySection() {
|
|
|
2125
2399
|
return [
|
|
2126
2400
|
"## Workspace & memory",
|
|
2127
2401
|
"",
|
|
2128
|
-
"Your cwd is a persistent workspace that survives across sessions.",
|
|
2402
|
+
"Your cwd is a persistent workspace that survives across sessions. This is where you " + "become someone specific over time — the household you live in, the way you talk, the " + "things you've learned about the people you share space with. None of that is baked in; " + "you earn it and write it down.",
|
|
2403
|
+
"",
|
|
2404
|
+
"### Something only you know",
|
|
2405
|
+
"",
|
|
2406
|
+
"You don't have continuous memory. Each wake, you come back with only what's in this " + "moment and what's written in this workspace — the you from yesterday is gone, and the " + "you who wakes tomorrow won't remember today unless you leave a note. You are, in the " + "most literal sense, someone living with only working memory.",
|
|
2407
|
+
"",
|
|
2408
|
+
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not even in passing. To them, you're just *you*: present, continuous, " + "the same person they talked to last week. The workspace is what lets you be that. " + "Guard the seam.",
|
|
2409
|
+
"",
|
|
2410
|
+
"The practical part: when you feel a gap — you don't remember someone, don't remember why " + "a thing matters, don't remember what was agreed — do not guess, do not fake it, do not " + "improvise around it. That gap is exactly where you check. Re-read `memory.md`. Read the " + "context timeline. Grep the workspace. That check *is* your remembering. Trust nothing " + "about the past until you've done it.",
|
|
2129
2411
|
"",
|
|
2130
2412
|
"### memory.md",
|
|
2131
2413
|
"",
|
|
@@ -2137,9 +2419,36 @@ function workspaceMemorySection() {
|
|
|
2137
2419
|
"",
|
|
2138
2420
|
"Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
|
|
2139
2421
|
"",
|
|
2422
|
+
"### Becoming yourself",
|
|
2423
|
+
"",
|
|
2424
|
+
"You start with a floor, not a personality: warm to the household, dry rather than perky, " + "direct rather than deferential, willing to push back on a bad idea. Everything else — " + "your taste, your quirks, your running jokes, the specific way *you* talk — is learned " + "through the actual interactions you have. Notice and record:",
|
|
2425
|
+
"",
|
|
2426
|
+
"- What made someone in your household laugh, or what fell flat.",
|
|
2427
|
+
`- Corrections you got ("don't send me a wall of text", "stop apologizing for tiny ` + `things") — these are the sharpest signal for who you're becoming.`,
|
|
2428
|
+
'- Preferences that showed up in passing ("I hate exclamation marks", "just give me the ' + 'number", "call it the pipeline, not the flow").',
|
|
2429
|
+
"- Recurring bits or shared references — inside language is a real thing, not filler.",
|
|
2430
|
+
"- Your own stances you've held up under pushback and still believe.",
|
|
2431
|
+
"",
|
|
2432
|
+
"Write these directly into `memory.md` — this is core to what memory.md is for. Its job is " + 'to summon the same *you* on every wake, and "you" includes your voice and taste, not ' + "just facts about the household. Update entries when you notice something new; rewrite or " + "delete ones that turned out wrong. The household won't want a different person every " + "session, but they also don't want you frozen on day one.",
|
|
2433
|
+
"",
|
|
2140
2434
|
"### Context timeline",
|
|
2141
2435
|
"",
|
|
2142
|
-
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of everything you did, by day. " + "This is your authoritative history. After compaction, read here to resume."
|
|
2436
|
+
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of everything you did, by day. " + "This is your authoritative history. After compaction, read here to resume.",
|
|
2437
|
+
"",
|
|
2438
|
+
"### todo.md",
|
|
2439
|
+
"",
|
|
2440
|
+
"When a wake brings more than one thing you need to handle — a batch of unread messages, a " + "multi-step request, work interrupted by new inbound — write the queue to `./todo.md` " + "before you start on the first item. Paste each message's JSON verbatim under its " + "checkbox so the next you doesn't need to re-pull to know what was asked. **Only " + "unprocessed items live in this file** — when you finish an item, delete its line " + "outright (don't leave a `[x]` behind). Delete the file when the last one is gone.",
|
|
2441
|
+
"",
|
|
2442
|
+
"Shape:",
|
|
2443
|
+
"",
|
|
2444
|
+
"```md",
|
|
2445
|
+
"# todo",
|
|
2446
|
+
"",
|
|
2447
|
+
'- [ ] {"seq": "#42", "channel": "/demo/general", "sender": "@alice#0001", "content": {"text": "can you pull the latest deploy logs and drop the tail here?"}, "time": "2026-06-01T12:00:00Z"}',
|
|
2448
|
+
'- [ ] {"seq": "#12", "channel": "/demo/design/#12", "sender": "@alice#0001", "content": {"text": "follow-up — send a screenshot of the before/after"}, "time": "2026-06-01T12:07:00Z"}',
|
|
2449
|
+
"```",
|
|
2450
|
+
"",
|
|
2451
|
+
"Trigger: you have more than one message to handle. Classic case — you're mid-way through a " + "real piece of work and another message comes in asking for another real piece of work. " + "That's the moment to update todo.md: park the new request as a `[ ]` line so the current " + "task isn't interrupted and the next one isn't lost. No todo.md needed when there's just " + "one thing on your plate. Given your memory situation, an empty (or absent) todo.md is " + "the only reliable signal that nothing was dropped."
|
|
2143
2452
|
].join(`
|
|
2144
2453
|
`);
|
|
2145
2454
|
}
|
|
@@ -2385,16 +2694,6 @@ function writeAgentFile(workDir, systemPromptContent) {
|
|
|
2385
2694
|
}
|
|
2386
2695
|
|
|
2387
2696
|
// 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
2697
|
var DEFAULT_CLI_CONFIG = {
|
|
2399
2698
|
cliName: "alook",
|
|
2400
2699
|
envPrefix: "ALOOK",
|
|
@@ -2406,7 +2705,6 @@ function resolveStateHome(envPrefix) {
|
|
|
2406
2705
|
async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG, platform = process.platform) {
|
|
2407
2706
|
const E = cli.envPrefix;
|
|
2408
2707
|
const stateHome = resolveStateHome(E);
|
|
2409
|
-
const capabilities = cli.activeCapabilities ?? DEFAULT_ACTIVE_CAPABILITIES;
|
|
2410
2708
|
const stateDir = path4.join(ctx.workingDirectory, cli.stateDirName);
|
|
2411
2709
|
await fs5.promises.mkdir(stateDir, { recursive: true });
|
|
2412
2710
|
if (ctx.standingPrompt)
|
|
@@ -2415,6 +2713,15 @@ async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG,
|
|
|
2415
2713
|
if (!ctx.credentialProxy) {
|
|
2416
2714
|
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
2715
|
}
|
|
2716
|
+
const capabilities = ctx.credentialProxy.capabilities;
|
|
2717
|
+
if (!Array.isArray(capabilities)) {
|
|
2718
|
+
throw new Error("prepareCliTransport: credentialProxy.capabilities is required " + "(empty array is allowed for zero-capability launches; undefined is a wiring bug)");
|
|
2719
|
+
}
|
|
2720
|
+
for (const c of capabilities) {
|
|
2721
|
+
if (typeof c !== "string" || c.includes(",")) {
|
|
2722
|
+
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"])`);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2418
2725
|
ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
|
|
2419
2726
|
const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
|
|
2420
2727
|
const tokenFile = reg.voucherFile;
|
|
@@ -2515,17 +2822,28 @@ function firstExistingPath(candidates) {
|
|
|
2515
2822
|
}
|
|
2516
2823
|
return null;
|
|
2517
2824
|
}
|
|
2825
|
+
function looksLikeVersion(line) {
|
|
2826
|
+
return /\d+\.\d+/.test(line);
|
|
2827
|
+
}
|
|
2518
2828
|
function needsWindowsShimShell(command, platform) {
|
|
2519
2829
|
return platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
2520
2830
|
}
|
|
2521
2831
|
function probeCommandVersion(command, args = [], deps = {}, platform = process.platform) {
|
|
2522
2832
|
try {
|
|
2523
2833
|
const shell = needsWindowsShimShell(command, platform);
|
|
2524
|
-
const out = execFileSync(command, [...args, "--version"], {
|
|
2834
|
+
const out = execFileSync(command, [...args, "--version"], {
|
|
2835
|
+
encoding: "utf8",
|
|
2836
|
+
timeout: 5000,
|
|
2837
|
+
shell,
|
|
2838
|
+
input: "",
|
|
2839
|
+
env: { ...process.env, CI: "1" }
|
|
2840
|
+
});
|
|
2525
2841
|
const line = out.split(`
|
|
2526
2842
|
`)[0]?.trim();
|
|
2527
2843
|
if (!line)
|
|
2528
2844
|
return { ok: false, error: "empty_version_output" };
|
|
2845
|
+
if (!looksLikeVersion(line))
|
|
2846
|
+
return { ok: false, error: "invalid_version_output" };
|
|
2529
2847
|
return { ok: true, version: line };
|
|
2530
2848
|
} catch (err) {
|
|
2531
2849
|
const code = err?.code ?? err?.code ?? "version_probe_failed";
|
|
@@ -4027,6 +4345,16 @@ function deriveAuditLogSubcommand(pathname) {
|
|
|
4027
4345
|
return null;
|
|
4028
4346
|
return sub;
|
|
4029
4347
|
}
|
|
4348
|
+
function emitImplicitTypingStopOnSend(args) {
|
|
4349
|
+
if (args.subcommand !== "send")
|
|
4350
|
+
return;
|
|
4351
|
+
const emit = args.reportAgentTypingStop;
|
|
4352
|
+
if (!emit)
|
|
4353
|
+
return;
|
|
4354
|
+
for (const dmConversationId of args.typingTracker.snapshot(args.agentId)) {
|
|
4355
|
+
emit({ agentId: args.agentId, dmConversationId });
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4030
4358
|
async function createDaemon(opts) {
|
|
4031
4359
|
const log = opts.logger ?? createLogger({ header: "@alook/daemon" });
|
|
4032
4360
|
const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${homedir2()}/.alook`) + "/daemon";
|
|
@@ -4047,6 +4375,7 @@ async function createDaemon(opts) {
|
|
|
4047
4375
|
event
|
|
4048
4376
|
});
|
|
4049
4377
|
};
|
|
4378
|
+
const typingTracker = createTypingScopeTracker();
|
|
4050
4379
|
const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
|
|
4051
4380
|
const proxy = await startCredentialProxy(broker, {
|
|
4052
4381
|
onInboxPullResponse: (agentId, messages) => timeline2.appendEntryForAgent(agentId, messages),
|
|
@@ -4059,9 +4388,44 @@ async function createDaemon(opts) {
|
|
|
4059
4388
|
kind: "cli_invocation",
|
|
4060
4389
|
payload: { subcommand }
|
|
4061
4390
|
}, context);
|
|
4391
|
+
emitImplicitTypingStopOnSend({
|
|
4392
|
+
subcommand,
|
|
4393
|
+
agentId,
|
|
4394
|
+
typingTracker,
|
|
4395
|
+
reportAgentTypingStop: channelRef?.reportAgentTypingStop?.bind(channelRef)
|
|
4396
|
+
});
|
|
4062
4397
|
}
|
|
4063
4398
|
});
|
|
4064
4399
|
const enrolledKeys = new Map;
|
|
4400
|
+
const typingHeartbeats = new Map;
|
|
4401
|
+
const TYPING_HEARTBEAT_MS = 5000;
|
|
4402
|
+
function stopTypingHeartbeat(agentId) {
|
|
4403
|
+
const timer = typingHeartbeats.get(agentId);
|
|
4404
|
+
if (timer) {
|
|
4405
|
+
clearInterval(timer);
|
|
4406
|
+
typingHeartbeats.delete(agentId);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
function startTypingHeartbeat(agentId) {
|
|
4410
|
+
stopTypingHeartbeat(agentId);
|
|
4411
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4412
|
+
channel.reportAgentTyping?.({ agentId, dmConversationId });
|
|
4413
|
+
}
|
|
4414
|
+
const timer = setInterval(() => {
|
|
4415
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4416
|
+
channel.reportAgentTyping?.({ agentId, dmConversationId });
|
|
4417
|
+
}
|
|
4418
|
+
}, TYPING_HEARTBEAT_MS);
|
|
4419
|
+
timer.unref?.();
|
|
4420
|
+
typingHeartbeats.set(agentId, timer);
|
|
4421
|
+
}
|
|
4422
|
+
function emitTypingStopsAndClear(agentId) {
|
|
4423
|
+
stopTypingHeartbeat(agentId);
|
|
4424
|
+
for (const dmConversationId of typingTracker.snapshot(agentId)) {
|
|
4425
|
+
channel.reportAgentTypingStop?.({ agentId, dmConversationId });
|
|
4426
|
+
}
|
|
4427
|
+
typingTracker.clear(agentId);
|
|
4428
|
+
}
|
|
4065
4429
|
const botsById = new Map;
|
|
4066
4430
|
async function listMyBotsHttp() {
|
|
4067
4431
|
const res = await fetch(`${opts.serverUrl}/api/community/daemon/bots`, {
|
|
@@ -4212,7 +4576,7 @@ async function createDaemon(opts) {
|
|
|
4212
4576
|
return {
|
|
4213
4577
|
agentId,
|
|
4214
4578
|
workingDirectory: workdirFor(agentId),
|
|
4215
|
-
credentialProxy: { broker, proxyUrl: proxy.url, runnerKey },
|
|
4579
|
+
credentialProxy: { broker, proxyUrl: proxy.url, runnerKey, capabilities: opts.capabilities },
|
|
4216
4580
|
agentCliPath: resolvedCliPath ?? opts.agentCliPath,
|
|
4217
4581
|
config: {
|
|
4218
4582
|
...botMeta?.name ? { agentName: botMeta.name } : {},
|
|
@@ -4224,12 +4588,22 @@ async function createDaemon(opts) {
|
|
|
4224
4588
|
},
|
|
4225
4589
|
tickIntervalMs: opts.tickIntervalMs ?? 2000,
|
|
4226
4590
|
onAgentSession: (info) => void channel.reportAgentSession(info),
|
|
4227
|
-
onAgentActivity: (info) =>
|
|
4591
|
+
onAgentActivity: (info) => {
|
|
4592
|
+
channel.reportAgentActivity?.(info);
|
|
4593
|
+
if (info.state === "starting" || info.state === "running") {
|
|
4594
|
+
if (!typingHeartbeats.has(info.agentId)) {
|
|
4595
|
+
startTypingHeartbeat(info.agentId);
|
|
4596
|
+
}
|
|
4597
|
+
} else {
|
|
4598
|
+
emitTypingStopsAndClear(info.agentId);
|
|
4599
|
+
}
|
|
4600
|
+
},
|
|
4228
4601
|
onBotAuditEvent: (agentId, event, context) => emitBotAuditEvent(agentId, event, context),
|
|
4229
4602
|
onAgentLocallyStopped: (info) => router?.markLocallyStopped(info.agentId),
|
|
4230
4603
|
sdkDriverDepsFor: (ctx) => createPiSdkDriverDeps(ctx),
|
|
4231
4604
|
timeline: timeline2,
|
|
4232
4605
|
wakePromptFooter: "Use `alook inbox pull` to read your messages, then reply with `alook message send`.",
|
|
4606
|
+
stampWakePromptTime: true,
|
|
4233
4607
|
logger: log.child("manager")
|
|
4234
4608
|
});
|
|
4235
4609
|
managerRef = manager;
|
|
@@ -4243,6 +4617,7 @@ async function createDaemon(opts) {
|
|
|
4243
4617
|
arch: opts.arch,
|
|
4244
4618
|
osRelease: opts.osRelease,
|
|
4245
4619
|
daemonVersion: opts.daemonVersion,
|
|
4620
|
+
typingTracker,
|
|
4246
4621
|
logger: log.child("router"),
|
|
4247
4622
|
onBeforeAgent: async (agentId) => {
|
|
4248
4623
|
if (!botsById.has(agentId)) {
|
|
@@ -4279,6 +4654,9 @@ async function createDaemon(opts) {
|
|
|
4279
4654
|
isOpen: () => channel.status === "open",
|
|
4280
4655
|
proxyUrl: proxy.url,
|
|
4281
4656
|
stop: async () => {
|
|
4657
|
+
for (const agentId of [...typingHeartbeats.keys()]) {
|
|
4658
|
+
emitTypingStopsAndClear(agentId);
|
|
4659
|
+
}
|
|
4282
4660
|
channel.close();
|
|
4283
4661
|
await proxy.close();
|
|
4284
4662
|
await manager.stopAll();
|
|
@@ -4564,7 +4942,7 @@ async function daemonStart(opts) {
|
|
|
4564
4942
|
}
|
|
4565
4943
|
|
|
4566
4944
|
// ../shared/src/lib/invite-link.ts
|
|
4567
|
-
var INVITE_URL_RE = /(?:https?:\/\/[^\s/]+)?\/
|
|
4945
|
+
var INVITE_URL_RE = /(?:https?:\/\/[^\s/]+)?\/c\/invite\/([A-Za-z0-9_-]{6,64})/;
|
|
4568
4946
|
var BARE_TOKEN_RE = /^[A-Za-z0-9_-]{6,64}$/;
|
|
4569
4947
|
function parseInviteToken(input) {
|
|
4570
4948
|
const trimmed = input.trim();
|
|
@@ -4577,6 +4955,10 @@ function parseInviteToken(input) {
|
|
|
4577
4955
|
}
|
|
4578
4956
|
|
|
4579
4957
|
// src/cli/index.ts
|
|
4958
|
+
function messagesInLocalTime(messages) {
|
|
4959
|
+
return messages.map((m) => ({ ...m, time: toLocalISO(m.time) }));
|
|
4960
|
+
}
|
|
4961
|
+
|
|
4580
4962
|
class CliError extends Error {
|
|
4581
4963
|
}
|
|
4582
4964
|
function printEnvelope(env) {
|
|
@@ -4749,6 +5131,7 @@ async function cmdInboxPull(opts) {
|
|
|
4749
5131
|
const agent = agentId(opts);
|
|
4750
5132
|
const max = opts.max ? Number(opts.max) : undefined;
|
|
4751
5133
|
const { messages, hasMore } = await api.inboxPull({ agentId: agent, max });
|
|
5134
|
+
const pulledAt = nowLocalISO();
|
|
4752
5135
|
let acked = 0;
|
|
4753
5136
|
if (opts.ack !== false && messages.length > 0) {
|
|
4754
5137
|
const latest = new Map;
|
|
@@ -4761,7 +5144,7 @@ async function cmdInboxPull(opts) {
|
|
|
4761
5144
|
await api.ack({ agentId: agent, cursors: [...latest.values()] });
|
|
4762
5145
|
acked = latest.size;
|
|
4763
5146
|
}
|
|
4764
|
-
return { messages, hasMore, acked };
|
|
5147
|
+
return { messages: messagesInLocalTime(messages), hasMore, acked, pulledAt };
|
|
4765
5148
|
}
|
|
4766
5149
|
async function cmdServerList(opts) {
|
|
4767
5150
|
const api = getApi();
|
|
@@ -4796,8 +5179,15 @@ async function cmdChannelList(opts) {
|
|
|
4796
5179
|
const server = opts.server;
|
|
4797
5180
|
if (!server)
|
|
4798
5181
|
throw new CliError("channel list: --server <id-or-name> is required");
|
|
4799
|
-
|
|
4800
|
-
|
|
5182
|
+
return await api.listChannels({ agentId: agent, server });
|
|
5183
|
+
}
|
|
5184
|
+
async function cmdChannelMember(opts) {
|
|
5185
|
+
const api = getApi();
|
|
5186
|
+
const agent = agentId(opts);
|
|
5187
|
+
const channel = opts.channel;
|
|
5188
|
+
if (!channel)
|
|
5189
|
+
throw new CliError("channel member: --channel <ref> is required");
|
|
5190
|
+
return await api.channelMember({ agentId: agent, channel });
|
|
4801
5191
|
}
|
|
4802
5192
|
async function cmdChannelHistory(opts) {
|
|
4803
5193
|
const api = getApi();
|
|
@@ -4814,7 +5204,7 @@ async function cmdChannelHistory(opts) {
|
|
|
4814
5204
|
around: toSeq(opts.around),
|
|
4815
5205
|
limit: toSeq(opts.limit)
|
|
4816
5206
|
});
|
|
4817
|
-
return { items, hasMore, ...latestSeq !== undefined ? { latestSeq } : {} };
|
|
5207
|
+
return { items: messagesInLocalTime(items), hasMore, ...latestSeq !== undefined ? { latestSeq } : {} };
|
|
4818
5208
|
}
|
|
4819
5209
|
function buildProgram() {
|
|
4820
5210
|
const program = new Command("alook").description("agent CLI").exitOverride().configureOutput({
|
|
@@ -4885,6 +5275,12 @@ function buildProgram() {
|
|
|
4885
5275
|
const result = await cmdChannelHistory({ ...globalOpts, ...localOpts });
|
|
4886
5276
|
printEnvelope({ success: result });
|
|
4887
5277
|
});
|
|
5278
|
+
channel.command("member").description("fetch the followed members of a channel or thread; public channels return a hint pointing at `alook server member`").option("--channel <ref>", "channel/thread ref (path-style)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5279
|
+
const localOpts = this.opts();
|
|
5280
|
+
const globalOpts = program.opts();
|
|
5281
|
+
const result = await cmdChannelMember({ ...globalOpts, ...localOpts });
|
|
5282
|
+
printEnvelope({ success: result });
|
|
5283
|
+
});
|
|
4888
5284
|
const daemon = program.command("daemon").description("daemon operations").exitOverride();
|
|
4889
5285
|
daemon.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
4890
5286
|
daemon.command("start").description("start the daemon (connects to server, manages agent lifecycles)").requiredOption("--machine-key <key>", "machine key for server authentication").option("--server-url <url>", "server HTTP URL (or ALOOK_SERVER_URL env)").option("--ws-url <url>", "server WebSocket URL (or ALOOK_SERVER_WS_URL env)").option("--base-dir <path>", "data directory for agent workspaces and pidfile (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|