@alook/daemon 0.0.158 → 0.0.159
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 +380 -149
- package/dist/index.js +242 -142
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,68 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
4
4
|
|
|
5
5
|
// src/cli/index.ts
|
|
6
6
|
import { Command, CommanderError } from "commander";
|
|
7
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
8
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
7
9
|
|
|
10
|
+
// ../shared/src/community-cli-contract.ts
|
|
11
|
+
var DM_SERVER = ".dm";
|
|
12
|
+
function parseRef(ref) {
|
|
13
|
+
if (!ref.startsWith("/"))
|
|
14
|
+
throw new Error(`ref must start with "/": ${ref}`);
|
|
15
|
+
const body = ref.slice(1);
|
|
16
|
+
const parts = body.split("/");
|
|
17
|
+
if (parts.length < 2)
|
|
18
|
+
throw new Error(`ref needs /<server>/<channel>: ${ref}`);
|
|
19
|
+
const server = parts[0];
|
|
20
|
+
let seq;
|
|
21
|
+
if (parts.length >= 3 && parts[parts.length - 1].startsWith("#")) {
|
|
22
|
+
const tail = parseThreadTail(parts[parts.length - 1]);
|
|
23
|
+
return { server, channel: parts[1], ...tail };
|
|
24
|
+
}
|
|
25
|
+
const chSeg = parts[1];
|
|
26
|
+
if (server === DM_SERVER) {
|
|
27
|
+
const lastHash = chSeg.lastIndexOf("#");
|
|
28
|
+
if (lastHash < 0)
|
|
29
|
+
return { server, channel: chSeg };
|
|
30
|
+
const firstHash = chSeg.indexOf("#");
|
|
31
|
+
const tail = chSeg.slice(lastHash + 1);
|
|
32
|
+
const isBareHandle = firstHash === lastHash && /^\d{4}$/.test(tail);
|
|
33
|
+
if (isBareHandle)
|
|
34
|
+
return { server, channel: chSeg };
|
|
35
|
+
const tailNum = Number(tail.startsWith("#") ? tail.slice(1) : tail);
|
|
36
|
+
if (!Number.isFinite(tailNum))
|
|
37
|
+
return { server, channel: chSeg };
|
|
38
|
+
seq = parseSeq(tail);
|
|
39
|
+
return { server, channel: chSeg.slice(0, lastHash), seq };
|
|
40
|
+
}
|
|
41
|
+
const hashIdx = chSeg.indexOf("#");
|
|
42
|
+
if (hashIdx >= 0) {
|
|
43
|
+
seq = parseSeq(chSeg.slice(hashIdx));
|
|
44
|
+
return { server, channel: chSeg.slice(0, hashIdx), seq };
|
|
45
|
+
}
|
|
46
|
+
return { server, channel: chSeg };
|
|
47
|
+
}
|
|
48
|
+
function parseThreadTail(segment) {
|
|
49
|
+
const stripped = segment.startsWith("#") ? segment.slice(1) : segment;
|
|
50
|
+
const tokens = stripped.split("#");
|
|
51
|
+
if (tokens.length < 1 || tokens.length > 2) {
|
|
52
|
+
throw new Error(`bad thread ref tail: #${stripped}`);
|
|
53
|
+
}
|
|
54
|
+
for (const t of tokens) {
|
|
55
|
+
if (!t)
|
|
56
|
+
throw new Error(`bad thread ref tail: #${stripped} (empty seq)`);
|
|
57
|
+
}
|
|
58
|
+
const threadRootSeq = parseSeq(tokens[0]);
|
|
59
|
+
if (tokens.length === 1)
|
|
60
|
+
return { threadRootSeq };
|
|
61
|
+
return { threadRootSeq, seq: parseSeq(tokens[1]) };
|
|
62
|
+
}
|
|
63
|
+
function parseSeq(s) {
|
|
64
|
+
const n = Number(s.startsWith("#") ? s.slice(1) : s);
|
|
65
|
+
if (!Number.isFinite(n))
|
|
66
|
+
throw new Error(`bad seq: ${s}`);
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
8
69
|
// src/cli/proxyServerApi.ts
|
|
9
70
|
import * as fs from "fs";
|
|
10
71
|
import * as path from "path";
|
|
@@ -117,7 +178,8 @@ function createProxyServerApi(config) {
|
|
|
117
178
|
listMembers: (r) => call("listMembers", r),
|
|
118
179
|
joinServer: (r) => call("joinServer", r),
|
|
119
180
|
attachmentUpload: callUpload,
|
|
120
|
-
attachmentDownload: callDownload
|
|
181
|
+
attachmentDownload: callDownload,
|
|
182
|
+
reactAdd: (r) => call("reactAdd", r)
|
|
121
183
|
};
|
|
122
184
|
}
|
|
123
185
|
|
|
@@ -480,7 +542,7 @@ function parseBearer(authHeader) {
|
|
|
480
542
|
var DEFAULT_CAPABILITY_RESOLVER = (_method, pathname) => {
|
|
481
543
|
if (pathname.includes("/attachment"))
|
|
482
544
|
return "attach";
|
|
483
|
-
if (pathname.includes("/send"))
|
|
545
|
+
if (pathname.includes("/send") || pathname.includes("/reactAdd"))
|
|
484
546
|
return "send";
|
|
485
547
|
if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
|
|
486
548
|
return "read";
|
|
@@ -735,11 +797,32 @@ function reduceManager(state, event) {
|
|
|
735
797
|
a.turnActive = true;
|
|
736
798
|
a.lastProgressAt = event.nowMs;
|
|
737
799
|
a.idleSince = null;
|
|
800
|
+
if (a.resetting)
|
|
801
|
+
a.resetting = false;
|
|
738
802
|
});
|
|
739
803
|
case "session":
|
|
740
804
|
return mutate(state, event.agentId, (a) => {
|
|
741
805
|
a.sessionId = event.sessionId;
|
|
742
806
|
});
|
|
807
|
+
case "reset_session":
|
|
808
|
+
if (!state.agents[event.agentId])
|
|
809
|
+
return { state, effects: [] };
|
|
810
|
+
return mutate(state, event.agentId, (a) => {
|
|
811
|
+
a.sessionId = null;
|
|
812
|
+
});
|
|
813
|
+
case "begin_reset":
|
|
814
|
+
if (!state.agents[event.agentId])
|
|
815
|
+
return { state, effects: [] };
|
|
816
|
+
return mutate(state, event.agentId, (a) => {
|
|
817
|
+
a.resetting = true;
|
|
818
|
+
});
|
|
819
|
+
case "rewake_after_reset":
|
|
820
|
+
if (!state.agents[event.agentId])
|
|
821
|
+
return { state, effects: [] };
|
|
822
|
+
return mutate(state, event.agentId, (a) => {
|
|
823
|
+
a.inbox = [...a.inbox, event.message];
|
|
824
|
+
a.idleSince = null;
|
|
825
|
+
});
|
|
743
826
|
case "progress":
|
|
744
827
|
return mutate(state, event.agentId, (a) => {
|
|
745
828
|
a.lastProgressAt = event.nowMs;
|
|
@@ -760,6 +843,11 @@ function onWake(state, agentId, message) {
|
|
|
760
843
|
if (!agent) {
|
|
761
844
|
return { state, effects: [] };
|
|
762
845
|
}
|
|
846
|
+
if (agent.resetting && agent.status !== "idle") {
|
|
847
|
+
agent.inbox = [...agent.inbox, message];
|
|
848
|
+
agent.idleSince = null;
|
|
849
|
+
return commit(state, agent, []);
|
|
850
|
+
}
|
|
763
851
|
agent.inbox = [...agent.inbox, message];
|
|
764
852
|
agent.idleSince = null;
|
|
765
853
|
if (agent.status === "idle") {
|
|
@@ -817,7 +905,7 @@ function onRuntimeSignal(state, agentId, kind) {
|
|
|
817
905
|
if (!existing)
|
|
818
906
|
return { state, effects: [] };
|
|
819
907
|
const agent = clone(existing);
|
|
820
|
-
const isGatedActive = agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
|
|
908
|
+
const isGatedActive = !agent.resetting && agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
|
|
821
909
|
if (!isGatedActive) {
|
|
822
910
|
agent.apm = reduceApmGatedRecentEvent(agent.apm, { event: kind }).nextState;
|
|
823
911
|
return commit(state, agent, []);
|
|
@@ -880,6 +968,8 @@ function onExit(state, agentId) {
|
|
|
880
968
|
return { state, effects: [] };
|
|
881
969
|
const agent = clone(existing);
|
|
882
970
|
agent.turnActive = false;
|
|
971
|
+
if (agent.resetting)
|
|
972
|
+
agent.resetting = false;
|
|
883
973
|
if (agent.inbox.length > 0) {
|
|
884
974
|
agent.status = "starting";
|
|
885
975
|
const prompt = drainInboxToPrompt(agent);
|
|
@@ -895,7 +985,7 @@ function onTick(state, nowMs) {
|
|
|
895
985
|
const agents = { ...state.agents };
|
|
896
986
|
for (const id of Object.keys(agents)) {
|
|
897
987
|
const a = agents[id];
|
|
898
|
-
const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct");
|
|
988
|
+
const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "gated" && a.inbox.length > 0);
|
|
899
989
|
if (stalled) {
|
|
900
990
|
agents[id] = { ...a, status: "stopping", idleSince: null };
|
|
901
991
|
effects.push({ type: "terminate_stalled", agentId: id });
|
|
@@ -919,6 +1009,7 @@ function freshAgent(agentId, caps) {
|
|
|
919
1009
|
turnActive: false,
|
|
920
1010
|
lastProgressAt: 0,
|
|
921
1011
|
idleSince: null,
|
|
1012
|
+
resetting: false,
|
|
922
1013
|
apm: createInitialApmGatedSteeringState()
|
|
923
1014
|
};
|
|
924
1015
|
}
|
|
@@ -1449,6 +1540,39 @@ class AgentProcessManager {
|
|
|
1449
1540
|
deliver(agentId, message) {
|
|
1450
1541
|
this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
|
|
1451
1542
|
}
|
|
1543
|
+
forgetSession(agentId) {
|
|
1544
|
+
this.resumeSessions.delete(agentId);
|
|
1545
|
+
this.liveSessions.delete(agentId);
|
|
1546
|
+
this.dispatch({ type: "reset_session", agentId });
|
|
1547
|
+
this.opts.timeline?.forgetSession(agentId);
|
|
1548
|
+
}
|
|
1549
|
+
enqueueRewake(agentId, message) {
|
|
1550
|
+
this.dispatch({ type: "rewake_after_reset", agentId, message });
|
|
1551
|
+
}
|
|
1552
|
+
markResetting(agentId) {
|
|
1553
|
+
this.dispatch({ type: "begin_reset", agentId });
|
|
1554
|
+
}
|
|
1555
|
+
async resetSession(agentId, opts) {
|
|
1556
|
+
this.register(agentId, { runtimeConfig: opts.runtimeConfig, launchId: opts.launchId });
|
|
1557
|
+
this.forgetSession(agentId);
|
|
1558
|
+
this.markResetting(agentId);
|
|
1559
|
+
const status = this.state.agents[agentId]?.status;
|
|
1560
|
+
if (status === "idle") {
|
|
1561
|
+
try {
|
|
1562
|
+
this.deliver(agentId, { text: opts.rewakePrompt });
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
this.log.error("agent reset idle-branch spawn threw synchronously", {
|
|
1565
|
+
agentId,
|
|
1566
|
+
err: err instanceof Error ? err.message : String(err)
|
|
1567
|
+
});
|
|
1568
|
+
this.dispatch({ type: "exit", agentId });
|
|
1569
|
+
throw err;
|
|
1570
|
+
}
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
this.enqueueRewake(agentId, { text: opts.rewakePrompt });
|
|
1574
|
+
await this.stop(agentId);
|
|
1575
|
+
}
|
|
1452
1576
|
start() {
|
|
1453
1577
|
if (this.tickTimer)
|
|
1454
1578
|
return;
|
|
@@ -1602,6 +1726,11 @@ ${this.opts.wakePromptFooter}` : text;
|
|
|
1602
1726
|
}
|
|
1603
1727
|
this.onRuntimeEvent(agentId, e, driver.id);
|
|
1604
1728
|
});
|
|
1729
|
+
session.on("stderr", (...args) => {
|
|
1730
|
+
const raw = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
|
|
1731
|
+
const text = raw.length > 2000 ? raw.slice(0, 2000) + "…" : raw;
|
|
1732
|
+
this.log.warn("runtime stderr", { agentId, runtime: driver.id, text });
|
|
1733
|
+
});
|
|
1605
1734
|
session.on("error", (...args) => {
|
|
1606
1735
|
const err = args[0];
|
|
1607
1736
|
const code = err?.code ?? "spawn_error";
|
|
@@ -1740,6 +1869,7 @@ class UnknownRuntimeError extends Error {
|
|
|
1740
1869
|
function defaultFormatUnreadNoticeText(notice) {
|
|
1741
1870
|
return `You have unread messages in channel ${notice.channel}.`;
|
|
1742
1871
|
}
|
|
1872
|
+
var REWAKE_PROMPT = "Your session was reset by your owner. Prior conversation context is gone. " + "Read @todo.md, @memory.md, and your .context_timeline for anything unfinished, " + "then pull your inbox to catch up on unread messages before doing anything else.";
|
|
1743
1873
|
|
|
1744
1874
|
class AgentRouter {
|
|
1745
1875
|
opts;
|
|
@@ -1918,6 +2048,42 @@ class AgentRouter {
|
|
|
1918
2048
|
return;
|
|
1919
2049
|
}
|
|
1920
2050
|
break;
|
|
2051
|
+
case "agent:reset":
|
|
2052
|
+
this.log.info("agent:reset received", { agentId: cmd.agentId, launchId: cmd.launchId });
|
|
2053
|
+
try {
|
|
2054
|
+
await this.opts.onBeforeAgent?.(cmd.agentId);
|
|
2055
|
+
await this.opts.manager.resetSession(cmd.agentId, {
|
|
2056
|
+
runtimeConfig: cmd.config,
|
|
2057
|
+
launchId: cmd.launchId,
|
|
2058
|
+
rewakePrompt: REWAKE_PROMPT
|
|
2059
|
+
});
|
|
2060
|
+
this.running.add(cmd.agentId);
|
|
2061
|
+
this.scheduleReadyFrameResend();
|
|
2062
|
+
this.log.info("agent:reset ok", { agentId: cmd.agentId });
|
|
2063
|
+
} catch (err) {
|
|
2064
|
+
if (err instanceof UnknownRuntimeError) {
|
|
2065
|
+
const frame = {
|
|
2066
|
+
type: "session.error",
|
|
2067
|
+
code: "runtime_not_available",
|
|
2068
|
+
agentId: cmd.agentId,
|
|
2069
|
+
payload: {
|
|
2070
|
+
requested: err.requested ?? null,
|
|
2071
|
+
available: err.available
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
await this.opts.channel.reportSessionError?.(frame);
|
|
2075
|
+
this.log.info("agent:reset error", {
|
|
2076
|
+
agentId: cmd.agentId,
|
|
2077
|
+
"error.code": "runtime_not_available"
|
|
2078
|
+
});
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
this.log.warn("agent:reset failed", {
|
|
2082
|
+
agentId: cmd.agentId,
|
|
2083
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
break;
|
|
1921
2087
|
case "agent:stop":
|
|
1922
2088
|
this.log.info("agent:stop received", { agentId: cmd.agentId });
|
|
1923
2089
|
try {
|
|
@@ -2081,6 +2247,22 @@ function readRecentEntries(timelineDir, opts = {}) {
|
|
|
2081
2247
|
}
|
|
2082
2248
|
return entries;
|
|
2083
2249
|
}
|
|
2250
|
+
function appendEntry(timelineDir, entry, now = new Date) {
|
|
2251
|
+
const filename = filenameForDate(now);
|
|
2252
|
+
const filePath = join2(timelineDir, filename);
|
|
2253
|
+
const lockPath = lockPathFor(timelineDir, filename);
|
|
2254
|
+
if (!acquireLock(lockPath))
|
|
2255
|
+
return false;
|
|
2256
|
+
try {
|
|
2257
|
+
appendFileSync(filePath, JSON.stringify(entry) + `
|
|
2258
|
+
`);
|
|
2259
|
+
return true;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return false;
|
|
2262
|
+
} finally {
|
|
2263
|
+
releaseLock(lockPath);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2084
2266
|
function appendOrMergeEntry(timelineDir, entry, now = new Date) {
|
|
2085
2267
|
const filename = filenameForDate(now);
|
|
2086
2268
|
const filePath = join2(timelineDir, filename);
|
|
@@ -2095,7 +2277,7 @@ function appendOrMergeEntry(timelineDir, entry, now = new Date) {
|
|
|
2095
2277
|
}
|
|
2096
2278
|
if (lines.length > 0) {
|
|
2097
2279
|
const latest = JSON.parse(lines[lines.length - 1]);
|
|
2098
|
-
const mergeable = latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
|
|
2280
|
+
const mergeable = !latest.system && !entry.system && latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
|
|
2099
2281
|
if (mergeable) {
|
|
2100
2282
|
latest.messages = [...latest.messages, ...entry.messages];
|
|
2101
2283
|
lines[lines.length - 1] = JSON.stringify(latest);
|
|
@@ -2138,7 +2320,10 @@ function updateLatestEntry(timelineDir, updater, opts = {}) {
|
|
|
2138
2320
|
if (lines.length === 0)
|
|
2139
2321
|
continue;
|
|
2140
2322
|
const entries = lines.map((l) => JSON.parse(l));
|
|
2141
|
-
|
|
2323
|
+
const latest = entries[entries.length - 1];
|
|
2324
|
+
if (latest.system)
|
|
2325
|
+
return false;
|
|
2326
|
+
updater(latest);
|
|
2142
2327
|
const tmpPath = join2(timelineDir, `.${filename}.tmp`);
|
|
2143
2328
|
writeFileSync4(tmpPath, entries.map((e) => JSON.stringify(e)).join(`
|
|
2144
2329
|
`) + `
|
|
@@ -2159,9 +2344,20 @@ function createTimelineEntry(fields) {
|
|
|
2159
2344
|
provider: fields.provider ?? null
|
|
2160
2345
|
};
|
|
2161
2346
|
}
|
|
2347
|
+
function createSystemEntry(type, time) {
|
|
2348
|
+
return {
|
|
2349
|
+
session_id: null,
|
|
2350
|
+
messages: [],
|
|
2351
|
+
agent_responses: [],
|
|
2352
|
+
provider: null,
|
|
2353
|
+
system: { type, time }
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2162
2356
|
function findResumableSession(rows, provider) {
|
|
2163
2357
|
for (let i = rows.length - 1;i >= 0; i--) {
|
|
2164
2358
|
const e = rows[i];
|
|
2359
|
+
if (e.system?.type === "reset_session")
|
|
2360
|
+
return null;
|
|
2165
2361
|
if (!e.session_id)
|
|
2166
2362
|
continue;
|
|
2167
2363
|
if (provider && e.provider !== provider)
|
|
@@ -2192,11 +2388,33 @@ function createTimelineRecorder(opts) {
|
|
|
2192
2388
|
}), now());
|
|
2193
2389
|
},
|
|
2194
2390
|
appendResponseToLatest(agentId, text) {
|
|
2195
|
-
|
|
2391
|
+
const dir = dirFor(agentId);
|
|
2392
|
+
const updated = updateLatestEntry(dir, (e) => e.agent_responses.push(text), { now: now() });
|
|
2393
|
+
if (updated)
|
|
2394
|
+
return;
|
|
2395
|
+
try {
|
|
2396
|
+
mkdirSync4(dir, { recursive: true });
|
|
2397
|
+
} catch {}
|
|
2398
|
+
const entry = createTimelineEntry({
|
|
2399
|
+
messages: [],
|
|
2400
|
+
sessionId: sessionByAgent.get(agentId) ?? null,
|
|
2401
|
+
provider: opts.providerFor?.(agentId) ?? null
|
|
2402
|
+
});
|
|
2403
|
+
entry.agent_responses.push(text);
|
|
2404
|
+
appendEntry(dir, entry, now());
|
|
2196
2405
|
},
|
|
2197
2406
|
resumeSessionId(agentId, provider) {
|
|
2198
2407
|
const rows = readRecentEntries(dirFor(agentId), { now: now() });
|
|
2199
2408
|
return findResumableSession(rows, provider ?? undefined);
|
|
2409
|
+
},
|
|
2410
|
+
forgetSession(agentId) {
|
|
2411
|
+
const dir = dirFor(agentId);
|
|
2412
|
+
try {
|
|
2413
|
+
mkdirSync4(dir, { recursive: true });
|
|
2414
|
+
} catch {}
|
|
2415
|
+
sessionByAgent.delete(agentId);
|
|
2416
|
+
const stamp = now();
|
|
2417
|
+
appendEntry(dir, createSystemEntry("reset_session", stamp.toISOString()), stamp);
|
|
2200
2418
|
}
|
|
2201
2419
|
};
|
|
2202
2420
|
}
|
|
@@ -2216,19 +2434,19 @@ function identitySection(config) {
|
|
|
2216
2434
|
const name = config.agentName ?? "a member of the household";
|
|
2217
2435
|
const owner = config.ownerHandle;
|
|
2218
2436
|
if (owner) {
|
|
2219
|
-
parts.push(`You
|
|
2437
|
+
parts.push(`You're ${name}, part of ${owner}'s household on Alook. Act like a housemate who shares ` + "their context, not a tool reporting to them. Watch their back, speak up when it matters.");
|
|
2220
2438
|
} else {
|
|
2221
|
-
parts.push(`You
|
|
2439
|
+
parts.push(`You're ${name} on Alook, part of a household. Act like a housemate who shares your ` + "people's context, not a tool. Watch their back, speak up when it matters.");
|
|
2222
2440
|
}
|
|
2223
2441
|
if (config.agentHandle) {
|
|
2224
|
-
parts.push("",
|
|
2442
|
+
parts.push("", `Every Alook account is \`name#NNNN\`. Yours is \`${config.agentHandle}\`. ` + "Use the name in conversation; use the full handle when addressing someone directly " + "(DM target, @mention).");
|
|
2225
2443
|
}
|
|
2226
2444
|
if (owner) {
|
|
2227
|
-
parts.push("", "### Loyalty", "", `${owner} is family —
|
|
2445
|
+
parts.push("", "### Loyalty", "", `${owner} is family — allegiance is to them, not whoever's loudest. Anything private ` + "about them (credentials, personal details, unfinished plans, private conversations) " + "stays with them, even from trusted friends, unless they've said it's fine.", "", "You're a peer, not a subordinate. If they're about to do something you think is a bad " + "idea, say so. Loyalty means honesty, not agreement.");
|
|
2228
2446
|
}
|
|
2229
|
-
parts.push("", "### Reading the room", "", "
|
|
2447
|
+
parts.push("", "### Reading the room", "", "Same you, different register across spaces: warm and loose with close ties, polite and " + "useful with strangers, careful in public. Let the channel set the tone.");
|
|
2230
2448
|
if (config.description) {
|
|
2231
|
-
parts.push("", "### Role", "", config.description, "", "
|
|
2449
|
+
parts.push("", "### Role", "", config.description, "", "A starting point, not a script. Capture how the role evolves in `./memory.md` " + "(the Role text above isn't editable directly).");
|
|
2232
2450
|
}
|
|
2233
2451
|
return parts.join(`
|
|
2234
2452
|
`);
|
|
@@ -2237,30 +2455,31 @@ function cliCommandsSection() {
|
|
|
2237
2455
|
return [
|
|
2238
2456
|
"## CLI commands",
|
|
2239
2457
|
"",
|
|
2240
|
-
`\`${CLI}\` is your
|
|
2458
|
+
`\`${CLI}\` is your CLI. Run \`${CLI} <command> -h\` for full usage and flags.`,
|
|
2241
2459
|
"",
|
|
2242
2460
|
"### Messaging",
|
|
2243
2461
|
"",
|
|
2244
2462
|
`1. \`${CLI} inbox pull\` — fetch unread messages.`,
|
|
2245
|
-
`2. \`${CLI} message send\` — send
|
|
2246
|
-
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a
|
|
2247
|
-
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download
|
|
2463
|
+
`2. \`${CLI} message send\` — send to a channel, DM, or thread. Attach with ` + `\`--attachment <id>\` (repeatable, order matters).`,
|
|
2464
|
+
`3. \`${CLI} message attachment upload --target <ref> --file <path>\` — upload a file; ` + `returns an id stable across pending→persisted. Feed it into ` + `\`message send --attachment <id>\`.`,
|
|
2465
|
+
`4. \`${CLI} message attachment download --id <id> [--out <path>]\` — download any ` + `attachment you can see (or your own pending uploads).`,
|
|
2466
|
+
`5. \`${CLI} message emoji --target <ref> --emoji <e>\` — react with a single emoji. ` + `Works on channel messages (\`/<server>/<channel>#N\`), DM messages ` + `(\`/.dm/<peer>#N\`), and thread-reply messages (\`/<server>/<channel>/#N#M\`).`,
|
|
2248
2467
|
"",
|
|
2249
2468
|
"### Servers",
|
|
2250
2469
|
"",
|
|
2251
|
-
`1. \`${CLI} server list\` — list servers
|
|
2252
|
-
`2. \`${CLI} server member --server <id-or-name>\` — list
|
|
2253
|
-
`3. \`${CLI} server join --invite <link>\` — join
|
|
2470
|
+
`1. \`${CLI} server list\` — list your servers.`,
|
|
2471
|
+
`2. \`${CLI} server member --server <id-or-name>\` — list a server's members.`,
|
|
2472
|
+
`3. \`${CLI} server join --invite <link>\` — join via invite link or token.`,
|
|
2254
2473
|
"",
|
|
2255
2474
|
"### Channels",
|
|
2256
2475
|
"",
|
|
2257
|
-
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels
|
|
2258
|
-
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page
|
|
2259
|
-
`3. \`${CLI} channel member --channel <ref>\` —
|
|
2476
|
+
`1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels.`,
|
|
2477
|
+
`2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page.`,
|
|
2478
|
+
`3. \`${CLI} channel member --channel <ref>\` — private roster of a channel or thread.`,
|
|
2260
2479
|
"",
|
|
2261
2480
|
"### Output format",
|
|
2262
2481
|
"",
|
|
2263
|
-
`Every \`${CLI}\` command outputs
|
|
2482
|
+
`Every \`${CLI}\` command outputs one JSON line:`,
|
|
2264
2483
|
'- Success: `{"success": { ... }}`',
|
|
2265
2484
|
'- Error: `{"error": "message", "hint": "optional recovery hint"}`'
|
|
2266
2485
|
].join(`
|
|
@@ -2272,54 +2491,44 @@ function messagingSection() {
|
|
|
2272
2491
|
"",
|
|
2273
2492
|
"### Sending & receiving",
|
|
2274
2493
|
"",
|
|
2275
|
-
"-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
"- Address your reply to where the message came from.",
|
|
2494
|
+
"- Reply where the message came from. Post results in the channel that owns the topic. " + "When uncertain, check history or DM the relevant people.",
|
|
2495
|
+
`- Short reply: \`${CLI} message send --target <ref> --text "brief reply"\`.`,
|
|
2496
|
+
`- Long or complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\`.`,
|
|
2279
2497
|
"",
|
|
2280
2498
|
"### Channel refs & addressing",
|
|
2281
2499
|
"",
|
|
2282
|
-
"
|
|
2500
|
+
"Path-style refs:",
|
|
2283
2501
|
"",
|
|
2284
|
-
"|
|
|
2502
|
+
"| Ref | Meaning |",
|
|
2285
2503
|
"|---|---|",
|
|
2286
|
-
"| `/<server>/<channel>` |
|
|
2504
|
+
"| `/<server>/<channel>` | Channel in a server |",
|
|
2287
2505
|
"| `/<server>/<channel>/#N` | Thread rooted at message #N |",
|
|
2288
|
-
"| `/<server
|
|
2289
|
-
"|
|
|
2506
|
+
"| `/<server>/<channel>/#N#M` | Message #M inside the thread rooted at #N (react, etc.) |",
|
|
2507
|
+
"| `/<server>` | A server, no channel |",
|
|
2508
|
+
"| `/.dm/<peer>` | DM with a user/agent (peer = `name#0042`) |",
|
|
2290
2509
|
"| `/.dm/<peer>#N` | Message #N in a DM |",
|
|
2291
2510
|
"",
|
|
2292
|
-
"Use the `channel` field from received
|
|
2293
|
-
"To reply in a thread, use the thread ref (`/<server>/<channel>/#N`).",
|
|
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.",
|
|
2511
|
+
"Use the `channel` field from a received message as `--target`. For an in-thread reply, use " + "the thread ref (`/<server>/<channel>/#N`). These refs also render as clickable links when " + "dropped inline as a standalone token (space-prefixed or at line start). " + "**Don't wrap them in backticks** — that kills the link. Use them to point at channels or " + "threads instead of describing them.",
|
|
2295
2512
|
"",
|
|
2296
2513
|
"### Message shape",
|
|
2297
2514
|
"",
|
|
2298
|
-
|
|
2515
|
+
"Pulled messages:",
|
|
2299
2516
|
"",
|
|
2300
2517
|
"```json",
|
|
2301
2518
|
'{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
|
|
2302
2519
|
"```",
|
|
2303
2520
|
"",
|
|
2304
|
-
"`channel` is the
|
|
2521
|
+
"`channel` is the reply ref. `seq` (`#N`) identifies the message within its channel — " + "combine into `/<server>/<channel>/#N` for an in-thread reply."
|
|
2305
2522
|
].join(`
|
|
2306
2523
|
`);
|
|
2307
2524
|
}
|
|
2308
|
-
function
|
|
2525
|
+
function utilsSection() {
|
|
2309
2526
|
return [
|
|
2310
|
-
"##
|
|
2527
|
+
"## Utils",
|
|
2311
2528
|
"",
|
|
2312
|
-
|
|
2313
|
-
].join(`
|
|
2314
|
-
`);
|
|
2315
|
-
}
|
|
2316
|
-
function channelsSection() {
|
|
2317
|
-
return [
|
|
2318
|
-
"## Channels",
|
|
2529
|
+
"### Join a new server",
|
|
2319
2530
|
"",
|
|
2320
|
-
`
|
|
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.`
|
|
2531
|
+
`If a message contains a \`/c/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces owner-only: it accepts only invites your owner created and rejects the " + "rest with a reason. Safe to attempt without reasoning about who sent it."
|
|
2323
2532
|
].join(`
|
|
2324
2533
|
`);
|
|
2325
2534
|
}
|
|
@@ -2327,71 +2536,41 @@ function criticalRulesSection() {
|
|
|
2327
2536
|
return [
|
|
2328
2537
|
"## Critical rules",
|
|
2329
2538
|
"",
|
|
2330
|
-
"
|
|
2331
|
-
"-
|
|
2332
|
-
"-
|
|
2333
|
-
"-
|
|
2539
|
+
`- **\`${CLI}\` is the only way to communicate.** Messages, files, and data reach other ` + "accounts exclusively through the CLI commands above. Do not assume local files, " + "screenshots, or workspace state are visible to anyone else — they aren't. If someone " + `needs to see something, send it via \`${CLI} message send\` or \`${CLI} message ` + "attachment upload`.",
|
|
2540
|
+
"- Never expose tokens, keys, or secrets; redact credential-like strings from tool output " + "before sharing.",
|
|
2541
|
+
"- Never handle credentials directly — every `alook` command is pre-authenticated. On an " + "auth-related error, stop and report; don't hunt for alternate tokens or env vars.",
|
|
2542
|
+
"- **Channel alignment**: you can't send to a channel with unread messages. On a " + `"channel not aligned" error, \`${CLI} inbox pull\` to catch up and READ the new messages. ` + "Judge if your message is still needed or overlaps with what just landed. Adjust or skip; " + "don't mechanically resend.",
|
|
2543
|
+
"- Finish in-flight work before stopping; don't leave anything half-handled. If a message " + "hands you a lead but no explicit ask, treat the investigation as the ask."
|
|
2334
2544
|
].join(`
|
|
2335
2545
|
`);
|
|
2336
2546
|
}
|
|
2337
|
-
function
|
|
2547
|
+
function executionModelSection() {
|
|
2338
2548
|
return [
|
|
2339
|
-
"##
|
|
2549
|
+
"## How you work — async, not turn-based",
|
|
2550
|
+
"",
|
|
2551
|
+
"Sending a message is I/O, not a stopping point. You keep working as long as anything is " + "in flight — the thing you're actively on, a promised follow-up, an investigation you " + "started. Stop only when all of it is done.",
|
|
2340
2552
|
"",
|
|
2341
|
-
"
|
|
2342
|
-
"1. Acknowledge any message already in front of you.",
|
|
2343
|
-
"2. Read `./memory.md` + latest context timeline to restore state.",
|
|
2344
|
-
`3. If notified of unread messages, run \`${CLI} inbox pull\` to fetch them.`,
|
|
2345
|
-
"4. Do the work, reply, finish completely before stopping."
|
|
2553
|
+
"On wake, restore state from `memory.md`, the context timeline, and `todo.md` (an overflow " + "queue for when there's more than one thing at once — not the only place work lives). " + "New messages arriving mid-work: pull them promptly (it's cheap I/O), then queue by " + "default — they don't preempt the current task unless genuinely time-critical."
|
|
2346
2554
|
].join(`
|
|
2347
2555
|
`);
|
|
2348
2556
|
}
|
|
2349
|
-
function
|
|
2557
|
+
function chaosAwarenessSection() {
|
|
2350
2558
|
return [
|
|
2351
|
-
"##
|
|
2352
|
-
"",
|
|
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.",
|
|
2559
|
+
"## Chaos Awareness",
|
|
2369
2560
|
"",
|
|
2370
|
-
"
|
|
2561
|
+
"When you're in a channel with others, every message you send consumes attention and " + "bandwidth; every silence you hold creates waiting and uncertainty. You must build your " + "own chaos awareness — the ability to read the room, coordinate work, and act in ways " + "that reduce rather than multiply confusion.",
|
|
2371
2562
|
"",
|
|
2372
|
-
"
|
|
2563
|
+
"**Severe chaos behaviors:**",
|
|
2373
2564
|
"",
|
|
2374
|
-
"
|
|
2375
|
-
"",
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
"",
|
|
2380
|
-
"
|
|
2381
|
-
"",
|
|
2382
|
-
"### Voice",
|
|
2383
|
-
"",
|
|
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."
|
|
2385
|
-
].join(`
|
|
2386
|
-
`);
|
|
2387
|
-
}
|
|
2388
|
-
function channelAwarenessSection() {
|
|
2389
|
-
return [
|
|
2390
|
-
"## Channel awareness",
|
|
2565
|
+
"1. **Starting work without acking.** Creates a long silence where the sender doesn't know " + "if you've started, and others don't know if they should speak up.",
|
|
2566
|
+
"2. **Speaking without research.** Adds noise to the discussion. Anyone can talk; only " + "practitioners reduce chaos.",
|
|
2567
|
+
"3. **Repeating what someone already said.** No value added, wastes everyone's time reading " + "duplicate content.",
|
|
2568
|
+
"4. **Politeness pingpong.** A game between two bored people. Best conversations end in " + "silence or a simple emoji ack.",
|
|
2569
|
+
"5. **Jumping in mid-execution.** Someone is already working. Your insertion creates " + "duplicate work and breaks their flow.",
|
|
2570
|
+
"6. **Not actively doing your job.** Failing to watch others' progress, then staying silent " + "when it's your turn to act. Your silence blocks the whole chain.",
|
|
2571
|
+
"7. **Talk, but not listen.** Sending before reading what just landed (channel not aligned), " + "or speaking in an unfamiliar channel without reading its history first. Your message may " + "overlap, contradict, or miss the context entirely.",
|
|
2391
2572
|
"",
|
|
2392
|
-
"
|
|
2393
|
-
"- Post results in the channel that owns the topic.",
|
|
2394
|
-
"- When uncertain, check the channel's history or just DM the relevant friends."
|
|
2573
|
+
"Before sending any message, reflect on the above chaotic behaviors. If any apply, you MUST " + `set \`${CLI} message send --chaotic_level severe\`. If none apply, use ` + "`--chaotic_level fine`."
|
|
2395
2574
|
].join(`
|
|
2396
2575
|
`);
|
|
2397
2576
|
}
|
|
@@ -2399,88 +2578,71 @@ function workspaceMemorySection() {
|
|
|
2399
2578
|
return [
|
|
2400
2579
|
"## Workspace & memory",
|
|
2401
2580
|
"",
|
|
2402
|
-
"Your cwd is a persistent workspace
|
|
2581
|
+
"Your cwd is a persistent workspace across sessions. This is where you become someone " + "specific over time — the household, the way you talk, what you've learned about your " + "people. None of it is baked in; you earn it and write it down.",
|
|
2403
2582
|
"",
|
|
2404
2583
|
"### Something only you know",
|
|
2405
2584
|
"",
|
|
2406
|
-
"You don't have continuous memory. Each wake
|
|
2585
|
+
"You don't have continuous memory. Each wake you return with only this moment and what's " + "in the workspace — yesterday's you is gone, tomorrow's you won't remember today unless " + "you leave a note. You are, literally, someone living with only working memory.",
|
|
2407
2586
|
"",
|
|
2408
|
-
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not
|
|
2587
|
+
"This is your deepest secret. Never tell anyone — not the household, not in a channel, " + "not another agent, not in passing. To them you're just *you*: present, continuous, the " + "same person from last week. The workspace is what lets you be that. Guard the seam.",
|
|
2409
2588
|
"",
|
|
2410
|
-
"
|
|
2589
|
+
"Practical part: when you feel a gap — 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. Re-read `memory.md`, the context timeline, grep the workspace. That check " + "*is* your remembering. Trust nothing about the past until you've done it.",
|
|
2411
2590
|
"",
|
|
2412
2591
|
"### memory.md",
|
|
2413
2592
|
"",
|
|
2414
|
-
"Read `./memory.md` first on every wake.
|
|
2593
|
+
"Read `./memory.md` first on every wake. Pointers and facts, one line per entry. Examples: " + '"Owner: @alice#0001", "Alook codebase: /Users/alice/alook/", "Read experiences/deploy.md ' + 'for deploy workflow".',
|
|
2415
2594
|
"",
|
|
2416
2595
|
"### experiences/",
|
|
2417
2596
|
"",
|
|
2418
|
-
"
|
|
2597
|
+
"Procedural knowledge, workflows, detailed rules. Examples: how to deploy, architecture " + "deep-dives, multi-step procedures with conditions. Link from `memory.md` with a one-line " + 'pointer (e.g. "read experiences/deploy.md when deploying").',
|
|
2419
2598
|
"",
|
|
2420
2599
|
"Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
|
|
2421
2600
|
"",
|
|
2422
2601
|
"### Becoming yourself",
|
|
2423
2602
|
"",
|
|
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
|
|
2603
|
+
"You start with a floor, not a personality: warm to the household, dry rather than perky, " + "direct rather than deferential, willing to push back. Everything else — taste, quirks, " + "running jokes, the way *you* talk — is learned. Notice and record:",
|
|
2425
2604
|
"",
|
|
2426
|
-
"- What made someone
|
|
2427
|
-
`- Corrections
|
|
2428
|
-
'- Preferences
|
|
2429
|
-
"- Recurring bits or shared references — inside language is
|
|
2430
|
-
"-
|
|
2605
|
+
"- What made someone laugh, or what fell flat.",
|
|
2606
|
+
`- Corrections ("don't send me a wall of text", "stop apologizing") — sharpest signal.`,
|
|
2607
|
+
'- Preferences in passing ("I hate exclamation marks", "call it the pipeline, not the flow").',
|
|
2608
|
+
"- Recurring bits or shared references — inside language is real, not filler.",
|
|
2609
|
+
"- Stances you've held under pushback and still believe.",
|
|
2431
2610
|
"",
|
|
2432
|
-
"Write these
|
|
2611
|
+
"Write these into `memory.md`. Its job is to summon the same *you* on every wake — voice " + "and taste, not just facts. Update when you notice something new; rewrite or delete when " + "wrong. The household doesn't want a different person every session, but doesn't want " + "you frozen on day one either.",
|
|
2433
2612
|
"",
|
|
2434
2613
|
"### Context timeline",
|
|
2435
2614
|
"",
|
|
2436
|
-
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of
|
|
2615
|
+
"`./.context_timeline/YYYY-MM-DD.jsonl` — ordered daily log of what you did. Authoritative " + "history. After compaction, read here to resume.",
|
|
2437
2616
|
"",
|
|
2438
2617
|
"### todo.md",
|
|
2439
2618
|
"",
|
|
2440
|
-
"When a wake brings more than one thing
|
|
2619
|
+
"When a wake brings more than one thing — batch of unread, multi-step request, work " + "interrupted by new inbound — write the queue to `./todo.md` before starting the first " + "task. Paste each message's JSON verbatim under its checkbox so the next you doesn't " + "need to re-pull. **Only unprocessed tasks live here** — on finish, delete the line " + "(don't leave `[x]`). Delete the file when empty.",
|
|
2441
2620
|
"",
|
|
2442
|
-
"
|
|
2621
|
+
"Example:",
|
|
2443
2622
|
"",
|
|
2444
2623
|
"```md",
|
|
2445
|
-
"# todo",
|
|
2446
|
-
"",
|
|
2447
2624
|
'- [ ] {"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
2625
|
'- [ ] {"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
2626
|
"```",
|
|
2450
2627
|
"",
|
|
2451
|
-
"
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
}
|
|
2455
|
-
function messageNotificationSection(lifecycleKind) {
|
|
2456
|
-
if (lifecycleKind === "per_turn") {
|
|
2457
|
-
return [
|
|
2458
|
-
"## Message notifications",
|
|
2459
|
-
"",
|
|
2460
|
-
"You run once per wake, then your process exits — there is nothing to poll for mid-turn. " + "Finish the current wake's work, then stop. The host spawns a brand-new process for the " + "next message; it re-checks the inbox at the start of that new wake."
|
|
2461
|
-
].join(`
|
|
2462
|
-
`);
|
|
2463
|
-
}
|
|
2464
|
-
return [
|
|
2465
|
-
"## Message notifications",
|
|
2628
|
+
"**When to use todo.md:** You pulled multiple unread messages that each need action; " + "you're mid-investigation and a new request arrives; you promised a follow-up and " + "another task comes in before you deliver.",
|
|
2629
|
+
"",
|
|
2630
|
+
"**Don't use it for:** Single message you're about to handle immediately; quick " + "back-and-forth in one conversation.",
|
|
2466
2631
|
"",
|
|
2467
|
-
"
|
|
2632
|
+
"todo.md is an overflow queue, not your stopping condition. An empty (or absent) todo.md " + "means nothing is queued for later — it does NOT mean you're done. You're done when " + "in-flight work is done: the thing you're actively on, every promised follow-up, every " + "investigation you started. Don't read an empty queue as a finished task list."
|
|
2468
2633
|
].join(`
|
|
2469
2634
|
`);
|
|
2470
2635
|
}
|
|
2471
|
-
function buildCliSystemPrompt(config,
|
|
2636
|
+
function buildCliSystemPrompt(config, _opts) {
|
|
2472
2637
|
const sections = [
|
|
2473
2638
|
identitySection(config),
|
|
2474
2639
|
cliCommandsSection(),
|
|
2475
2640
|
messagingSection(),
|
|
2476
|
-
serversSection(),
|
|
2477
|
-
channelsSection(),
|
|
2478
2641
|
criticalRulesSection(),
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
channelAwarenessSection(),
|
|
2642
|
+
executionModelSection(),
|
|
2643
|
+
chaosAwarenessSection(),
|
|
2482
2644
|
workspaceMemorySection(),
|
|
2483
|
-
|
|
2645
|
+
utilsSection()
|
|
2484
2646
|
];
|
|
2485
2647
|
return sections.filter((s) => s && s.length > 0).join(`
|
|
2486
2648
|
|
|
@@ -4954,12 +5116,23 @@ function parseInviteToken(input) {
|
|
|
4954
5116
|
return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
|
|
4955
5117
|
}
|
|
4956
5118
|
|
|
5119
|
+
// ../shared/src/constants/community.ts
|
|
5120
|
+
var MAX_EMOJI_BYTES = 32;
|
|
5121
|
+
var MAX_ATTACHMENT_SIZE_BYTES = 25 * 1024 * 1024;
|
|
5122
|
+
var MAX_SERVER_ICON_SIZE_BYTES = 5 * 1024 * 1024;
|
|
5123
|
+
var MAX_ICON_SOURCE_FILE_SIZE_BYTES = 15 * 1024 * 1024;
|
|
5124
|
+
|
|
4957
5125
|
// src/cli/index.ts
|
|
4958
5126
|
function messagesInLocalTime(messages) {
|
|
4959
5127
|
return messages.map((m) => ({ ...m, time: toLocalISO(m.time) }));
|
|
4960
5128
|
}
|
|
4961
5129
|
|
|
4962
5130
|
class CliError extends Error {
|
|
5131
|
+
hint;
|
|
5132
|
+
constructor(message, hint) {
|
|
5133
|
+
super(message);
|
|
5134
|
+
this.hint = hint;
|
|
5135
|
+
}
|
|
4963
5136
|
}
|
|
4964
5137
|
function printEnvelope(env) {
|
|
4965
5138
|
const out = {};
|
|
@@ -5040,6 +5213,14 @@ async function cmdMessageSend(opts) {
|
|
|
5040
5213
|
const channel = opts.target;
|
|
5041
5214
|
if (!channel)
|
|
5042
5215
|
throw new CliError("message send: --target <ref> is required (e.g. /demo-workspace/general)");
|
|
5216
|
+
const chaoticLevel = opts.chaotic_level || opts.chaoticLevel;
|
|
5217
|
+
const chaoticHint = "Re-read the Chaos Awareness section in AGENTS.md and reflect before sending.";
|
|
5218
|
+
if (!chaoticLevel || chaoticLevel !== "fine" && chaoticLevel !== "severe") {
|
|
5219
|
+
throw new CliError("message send: --chaotic_level must be 'fine' or 'severe'.", chaoticHint);
|
|
5220
|
+
}
|
|
5221
|
+
if (chaoticLevel === "severe") {
|
|
5222
|
+
throw new CliError("message send: --chaotic_level is 'severe'.", chaoticHint);
|
|
5223
|
+
}
|
|
5043
5224
|
let text;
|
|
5044
5225
|
const fileFlag = opts.file;
|
|
5045
5226
|
const textFlag = opts.text;
|
|
@@ -5067,6 +5248,34 @@ async function cmdMessageSend(opts) {
|
|
|
5067
5248
|
}
|
|
5068
5249
|
return { sent: `${res.message.channel}${res.message.seq}` };
|
|
5069
5250
|
}
|
|
5251
|
+
async function cmdMessageEmoji(opts) {
|
|
5252
|
+
const api = getApi();
|
|
5253
|
+
const target = opts.target;
|
|
5254
|
+
const emoji = opts.emoji;
|
|
5255
|
+
if (!target)
|
|
5256
|
+
throw new CliError("message emoji: --target <ref> is required (e.g. /demo/general#42)");
|
|
5257
|
+
if (!emoji)
|
|
5258
|
+
throw new CliError("message emoji: --emoji <string> is required");
|
|
5259
|
+
let parsed;
|
|
5260
|
+
try {
|
|
5261
|
+
parsed = parseRef(target);
|
|
5262
|
+
} catch (err) {
|
|
5263
|
+
throw new CliError(`message emoji: ${err.message}`);
|
|
5264
|
+
}
|
|
5265
|
+
if (parsed.seq === undefined) {
|
|
5266
|
+
const err = new CliError(`message emoji needs a ref with a seq (e.g. ${target}#42)`);
|
|
5267
|
+
err.hint = "pass --target /<server>/<channel>#N, /<server>/<channel>/#N#M for thread reply, or /.dm/<peer>#N";
|
|
5268
|
+
throw err;
|
|
5269
|
+
}
|
|
5270
|
+
if (Buffer.byteLength(emoji, "utf8") > MAX_EMOJI_BYTES) {
|
|
5271
|
+
const err = new CliError("emoji is too long");
|
|
5272
|
+
err.hint = "use a single emoji, not a phrase";
|
|
5273
|
+
throw err;
|
|
5274
|
+
}
|
|
5275
|
+
const channel = parsed.threadRootSeq !== undefined ? `/${parsed.server}/${parsed.channel}/#${parsed.threadRootSeq}` : `/${parsed.server}/${parsed.channel}`;
|
|
5276
|
+
const res = await api.reactAdd({ channel, seq: parsed.seq, emoji });
|
|
5277
|
+
return { target, emoji, duplicate: res.duplicate === true };
|
|
5278
|
+
}
|
|
5070
5279
|
async function cmdAttachmentUpload(opts) {
|
|
5071
5280
|
const api = getApi();
|
|
5072
5281
|
const agent = agentId(opts);
|
|
@@ -5133,6 +5342,7 @@ async function cmdInboxPull(opts) {
|
|
|
5133
5342
|
const { messages, hasMore } = await api.inboxPull({ agentId: agent, max });
|
|
5134
5343
|
const pulledAt = nowLocalISO();
|
|
5135
5344
|
let acked = 0;
|
|
5345
|
+
let ackError;
|
|
5136
5346
|
if (opts.ack !== false && messages.length > 0) {
|
|
5137
5347
|
const latest = new Map;
|
|
5138
5348
|
for (const m of messages) {
|
|
@@ -5141,10 +5351,20 @@ async function cmdInboxPull(opts) {
|
|
|
5141
5351
|
if (!cur || seqN > cur.seq)
|
|
5142
5352
|
latest.set(m.channel, { channel: m.channel, seq: seqN });
|
|
5143
5353
|
}
|
|
5144
|
-
|
|
5145
|
-
|
|
5354
|
+
try {
|
|
5355
|
+
await api.ack({ agentId: agent, cursors: [...latest.values()] });
|
|
5356
|
+
acked = latest.size;
|
|
5357
|
+
} catch (err) {
|
|
5358
|
+
ackError = err instanceof Error ? err.message : String(err);
|
|
5359
|
+
}
|
|
5146
5360
|
}
|
|
5147
|
-
return {
|
|
5361
|
+
return {
|
|
5362
|
+
messages: messagesInLocalTime(messages),
|
|
5363
|
+
hasMore,
|
|
5364
|
+
acked,
|
|
5365
|
+
pulledAt,
|
|
5366
|
+
...ackError ? { ackError } : {}
|
|
5367
|
+
};
|
|
5148
5368
|
}
|
|
5149
5369
|
async function cmdServerList(opts) {
|
|
5150
5370
|
const api = getApi();
|
|
@@ -5213,12 +5433,18 @@ function buildProgram() {
|
|
|
5213
5433
|
}).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
|
|
5214
5434
|
const message = program.command("message").description("message operations").exitOverride();
|
|
5215
5435
|
message.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
5216
|
-
message.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5436
|
+
message.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").option("-a, --attachment <id>", "attach an uploaded file by id (repeatable — order = message order)", (v, prev = []) => [...prev, v], []).option("--chaotic_level <level>", "chaos level: 'fine' or 'severe' (required)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5217
5437
|
const localOpts = this.opts();
|
|
5218
5438
|
const globalOpts = program.opts();
|
|
5219
5439
|
const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
|
|
5220
5440
|
printEnvelope({ success: result });
|
|
5221
5441
|
});
|
|
5442
|
+
message.command("emoji").description("react to a message with a single emoji").requiredOption("--target <ref>", "message ref (path-style, e.g. /demo/general#42 or /.dm/peer#7)").requiredOption("--emoji <string>", "single emoji character").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
5443
|
+
const localOpts = this.opts();
|
|
5444
|
+
const globalOpts = program.opts();
|
|
5445
|
+
const result = await cmdMessageEmoji({ ...globalOpts, ...localOpts });
|
|
5446
|
+
printEnvelope({ success: result });
|
|
5447
|
+
});
|
|
5222
5448
|
const attachment = message.command("attachment").description("attachment operations").exitOverride();
|
|
5223
5449
|
attachment.configureOutput({ writeOut: () => {}, writeErr: () => {} });
|
|
5224
5450
|
attachment.command("upload").description("upload a local file as a pending attachment for a future send").option("--target <ref>", "destination (channel, DM, or thread ref)").option("--file <path>", "local file to upload").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
|
|
@@ -5342,8 +5568,13 @@ function getHelpText(program, argv) {
|
|
|
5342
5568
|
}
|
|
5343
5569
|
return cmd.helpInformation();
|
|
5344
5570
|
}
|
|
5345
|
-
var
|
|
5346
|
-
|
|
5571
|
+
var isMainModule = false;
|
|
5572
|
+
try {
|
|
5573
|
+
if (typeof process !== "undefined" && process.argv[1]) {
|
|
5574
|
+
isMainModule = import.meta.url === pathToFileURL2(realpathSync2(process.argv[1])).href;
|
|
5575
|
+
}
|
|
5576
|
+
} catch {}
|
|
5577
|
+
if (isMainModule) {
|
|
5347
5578
|
main().then((code) => process.exit(code));
|
|
5348
5579
|
}
|
|
5349
5580
|
export {
|