ei-tui 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/README.md +25 -33
- package/src/cli/mcp.ts +3 -123
- package/src/cli/retrieval.ts +3 -34
- package/src/cli.ts +283 -26
- package/src/core/heartbeat-manager.ts +58 -4
- package/src/core/orchestrators/ceremony.ts +5 -3
- package/src/core/processor.ts +50 -13
- package/src/core/tools/builtin/find-memory.ts +1 -1
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/entities.ts +2 -0
- package/src/integrations/slack/importer.ts +36 -15
- package/src/integrations/slack/reader.ts +23 -10
- package/src/integrations/slack/types.ts +27 -9
- package/src/prompts/heartbeat/check.ts +7 -2
- package/src/prompts/heartbeat/ei.ts +34 -12
- package/src/prompts/heartbeat/index.ts +1 -0
- package/src/prompts/heartbeat/types.ts +6 -3
- package/src/prompts/index.ts +1 -1
- package/src/prompts/message-utils.ts +1 -1
- package/tui/src/commands/slack-auth.ts +13 -7
- package/tui/src/util/yaml-settings.ts +38 -10
|
@@ -3,11 +3,12 @@ import type { Ei_Interface, Message, PersonaEntity, Person } from "../../core/ty
|
|
|
3
3
|
import type { PersonIdentifier } from "../../core/types/data-items.js";
|
|
4
4
|
import { ContextStatus } from "../../core/types/enums.js";
|
|
5
5
|
import { queueAllScans, queuePersonScan, queuePersonUpdate, type ExtractionContext } from "../../core/orchestrators/human-extraction.js";
|
|
6
|
+
import { queueTopicRewritePhase } from "../../core/orchestrators/ceremony.js";
|
|
6
7
|
import type { ItemMatchResult } from "../../prompts/human/types.js";
|
|
7
8
|
import { qualifySlackMessage } from "../../core/utils/message-id.js";
|
|
8
9
|
import { SLACK_PERSONA_DEFINITION } from "../../templates/slack.js";
|
|
9
10
|
import { SlackReader, SlackRateLimitError, type ResolvedMessage } from "./reader.js";
|
|
10
|
-
import type { SlackChannelState } from "./types.js";
|
|
11
|
+
import type { SlackChannelState, SlackWorkspaceConfig } from "./types.js";
|
|
11
12
|
|
|
12
13
|
const SLACK_USER_ID_KEY = "Slack User ID";
|
|
13
14
|
const WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
@@ -170,10 +171,19 @@ export async function importSlackChannel(opts: {
|
|
|
170
171
|
|
|
171
172
|
const human = stateManager.getHuman();
|
|
172
173
|
const slackSettings = human.settings?.slack;
|
|
173
|
-
|
|
174
|
+
const workspaces = slackSettings?.workspaces ?? {};
|
|
175
|
+
|
|
176
|
+
// Find the workspace with the oldest unprocessed channel that has integration enabled
|
|
177
|
+
const enabledWorkspaces = Object.entries(workspaces).filter(([, ws]) => ws.integration);
|
|
178
|
+
if (enabledWorkspaces.length === 0) return result;
|
|
179
|
+
|
|
180
|
+
// We'll pick the right workspace after channel discovery — for now grab the first enabled one
|
|
181
|
+
// to bootstrap the reader. Multi-workspace candidate selection happens below.
|
|
182
|
+
// TODO: proper cross-workspace oldest-channel selection in a future pass
|
|
183
|
+
const [workspaceId, workspaceConfig] = enabledWorkspaces[0] as [string, SlackWorkspaceConfig];
|
|
174
184
|
|
|
175
185
|
const persona = ensureSlackPersona(stateManager, opts.interface);
|
|
176
|
-
const reader = new SlackReader(
|
|
186
|
+
const reader = new SlackReader(workspaceConfig.auth);
|
|
177
187
|
|
|
178
188
|
// Seed caches from known people identifiers
|
|
179
189
|
for (const person of human.people) {
|
|
@@ -190,15 +200,13 @@ export async function importSlackChannel(opts: {
|
|
|
190
200
|
if (signal?.aborted) return result;
|
|
191
201
|
|
|
192
202
|
let channels = await reader.listChannels();
|
|
193
|
-
const channelStates: Record<string, SlackChannelState> = { ...
|
|
203
|
+
const channelStates: Record<string, SlackChannelState> = { ...workspaceConfig.channels };
|
|
194
204
|
|
|
195
205
|
// Seed channel name cache from saved state
|
|
196
206
|
for (const [id, state] of Object.entries(channelStates)) {
|
|
197
207
|
if (state.name) reader.seedChannelCache(id, state.name);
|
|
198
208
|
}
|
|
199
209
|
|
|
200
|
-
const workspaceId = slackSettings.auth?.workspace_id ?? "unknown";
|
|
201
|
-
|
|
202
210
|
// Loop through candidate channels until we find one with messages to process,
|
|
203
211
|
// or exhaust all candidates. Empty channels are marked caught up and skipped
|
|
204
212
|
// so the next cycle doesn't re-examine them.
|
|
@@ -214,7 +222,7 @@ export async function importSlackChannel(opts: {
|
|
|
214
222
|
while (true) {
|
|
215
223
|
if (signal?.aborted) return result;
|
|
216
224
|
|
|
217
|
-
const candidate = reader.selectCandidateChannel(channels, channelStates,
|
|
225
|
+
const candidate = reader.selectCandidateChannel(channels, channelStates, workspaceConfig, now);
|
|
218
226
|
if (!candidate) return result; // all channels caught up
|
|
219
227
|
|
|
220
228
|
const { channel, state } = candidate;
|
|
@@ -228,7 +236,7 @@ export async function importSlackChannel(opts: {
|
|
|
228
236
|
channelName = updatedState.name ?? channelId;
|
|
229
237
|
reader.seedChannelCache(channelId, channelName);
|
|
230
238
|
|
|
231
|
-
const extractionPointMs = new Date(channelState.extraction_point ?? new Date(nowMs - (
|
|
239
|
+
const extractionPointMs = new Date(channelState.extraction_point ?? new Date(nowMs - (workspaceConfig.backfill_days?.public ?? 30) * 86400_000).toISOString()).getTime();
|
|
232
240
|
const extractionPointTs = (extractionPointMs / 1000).toFixed(6);
|
|
233
241
|
|
|
234
242
|
// Probe for the next actual message — skips silent periods instantly
|
|
@@ -256,7 +264,13 @@ export async function importSlackChannel(opts: {
|
|
|
256
264
|
...updatedHuman.settings,
|
|
257
265
|
slack: {
|
|
258
266
|
...updatedHuman.settings?.slack,
|
|
259
|
-
|
|
267
|
+
workspaces: {
|
|
268
|
+
...updatedHuman.settings?.slack?.workspaces,
|
|
269
|
+
[workspaceId]: {
|
|
270
|
+
...workspaceConfig,
|
|
271
|
+
channels: { ...workspaceConfig.channels, [channelId]: updatedState },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
260
274
|
},
|
|
261
275
|
},
|
|
262
276
|
});
|
|
@@ -323,7 +337,7 @@ export async function importSlackChannel(opts: {
|
|
|
323
337
|
sourceTag,
|
|
324
338
|
workspaceId,
|
|
325
339
|
stateManager,
|
|
326
|
-
|
|
340
|
+
workspaceConfig.extraction_model,
|
|
327
341
|
);
|
|
328
342
|
}
|
|
329
343
|
|
|
@@ -347,7 +361,7 @@ export async function importSlackChannel(opts: {
|
|
|
347
361
|
result.scansQueued += queueScansForMessages(
|
|
348
362
|
contextMsgs, analyzeMsgs, participants,
|
|
349
363
|
persona.id, channelName, sourceTag, workspaceId,
|
|
350
|
-
stateManager,
|
|
364
|
+
stateManager, workspaceConfig.extraction_model,
|
|
351
365
|
);
|
|
352
366
|
}
|
|
353
367
|
|
|
@@ -375,7 +389,7 @@ export async function importSlackChannel(opts: {
|
|
|
375
389
|
result.scansQueued += queueScansForMessages(
|
|
376
390
|
contextMsgs, analyzeMsgs, participants,
|
|
377
391
|
persona.id, channelName, sourceTag, workspaceId,
|
|
378
|
-
stateManager,
|
|
392
|
+
stateManager, workspaceConfig.extraction_model,
|
|
379
393
|
);
|
|
380
394
|
}
|
|
381
395
|
|
|
@@ -395,14 +409,21 @@ export async function importSlackChannel(opts: {
|
|
|
395
409
|
...updatedHuman.settings,
|
|
396
410
|
slack: {
|
|
397
411
|
...updatedHuman.settings?.slack,
|
|
398
|
-
|
|
399
|
-
...updatedHuman.settings?.slack?.
|
|
400
|
-
[
|
|
412
|
+
workspaces: {
|
|
413
|
+
...updatedHuman.settings?.slack?.workspaces,
|
|
414
|
+
[workspaceId]: {
|
|
415
|
+
...workspaceConfig,
|
|
416
|
+
channels: { ...workspaceConfig.channels, [channelId]: updatedState },
|
|
417
|
+
},
|
|
401
418
|
},
|
|
402
419
|
},
|
|
403
420
|
},
|
|
404
421
|
});
|
|
405
422
|
|
|
423
|
+
if (result.messagesImported > 0) {
|
|
424
|
+
queueTopicRewritePhase(stateManager);
|
|
425
|
+
}
|
|
426
|
+
|
|
406
427
|
result.channelProcessed = channelName;
|
|
407
428
|
return result;
|
|
408
429
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SlackChannelState,
|
|
1
|
+
import type { SlackAuth, SlackChannelState, SlackWorkspaceConfig } from "./types.js";
|
|
2
2
|
|
|
3
3
|
export class SlackRateLimitError extends Error {
|
|
4
4
|
constructor(method: string) {
|
|
@@ -7,6 +7,14 @@ export class SlackRateLimitError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function resolveEnvVar(value: string): string {
|
|
11
|
+
if (value.startsWith("$")) {
|
|
12
|
+
const name = value.slice(1);
|
|
13
|
+
return process.env[name] ?? value;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
// =============================================================================
|
|
11
19
|
// Slack API types
|
|
12
20
|
// =============================================================================
|
|
@@ -75,12 +83,12 @@ export interface ResolvedMessage {
|
|
|
75
83
|
// =============================================================================
|
|
76
84
|
|
|
77
85
|
export class SlackReader {
|
|
78
|
-
private
|
|
86
|
+
private auth: SlackAuth;
|
|
79
87
|
private userCache: Map<string, string> = new Map(); // userId → displayName
|
|
80
88
|
private channelCache: Map<string, string> = new Map(); // channelId → name
|
|
81
89
|
|
|
82
|
-
constructor(
|
|
83
|
-
this.
|
|
90
|
+
constructor(auth: SlackAuth) {
|
|
91
|
+
this.auth = auth;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
// ---------------------------------------------------------------------------
|
|
@@ -92,9 +100,14 @@ export class SlackReader {
|
|
|
92
100
|
for (const [k, v] of Object.entries(params)) {
|
|
93
101
|
url.searchParams.set(k, String(v));
|
|
94
102
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
const headers: Record<string, string> = {};
|
|
104
|
+
if (this.auth.type === "browser") {
|
|
105
|
+
headers["Authorization"] = `Bearer ${resolveEnvVar(this.auth.xoxc)}`;
|
|
106
|
+
headers["Cookie"] = `d=${resolveEnvVar(this.auth.xoxd)}`;
|
|
107
|
+
} else {
|
|
108
|
+
headers["Authorization"] = `Bearer ${this.auth.token}`;
|
|
109
|
+
}
|
|
110
|
+
const resp = await fetch(url.toString(), { headers });
|
|
98
111
|
if (resp.status === 429) throw new SlackRateLimitError(method);
|
|
99
112
|
if (!resp.ok) throw new Error(`Slack API ${method} failed: ${resp.status}`);
|
|
100
113
|
const data = await resp.json() as Record<string, unknown>;
|
|
@@ -123,7 +136,7 @@ export class SlackReader {
|
|
|
123
136
|
return allChannels;
|
|
124
137
|
}
|
|
125
138
|
|
|
126
|
-
classifyChannel(ch: SlackChannel, settings:
|
|
139
|
+
classifyChannel(ch: SlackChannel, settings: SlackWorkspaceConfig): ChannelTier {
|
|
127
140
|
const override = settings.channel_overrides?.[ch.id];
|
|
128
141
|
if (override) return override === "skip" ? "skip" : override;
|
|
129
142
|
if (ch.is_im || ch.is_mpim) return "dm";
|
|
@@ -133,7 +146,7 @@ export class SlackReader {
|
|
|
133
146
|
return "public";
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
backfillDaysForTier(tier: ChannelTier, settings:
|
|
149
|
+
backfillDaysForTier(tier: ChannelTier, settings: SlackWorkspaceConfig): number {
|
|
137
150
|
const defaults = { dm: 90, private: 90, public: 30 };
|
|
138
151
|
if (tier === "dm" || tier === "private") return settings.backfill_days?.dm ?? defaults[tier];
|
|
139
152
|
if (tier === "public") return settings.backfill_days?.public ?? defaults.public;
|
|
@@ -387,7 +400,7 @@ export class SlackReader {
|
|
|
387
400
|
selectCandidateChannel(
|
|
388
401
|
channels: SlackChannel[],
|
|
389
402
|
channelStates: Record<string, SlackChannelState>,
|
|
390
|
-
settings:
|
|
403
|
+
settings: SlackWorkspaceConfig,
|
|
391
404
|
now: string,
|
|
392
405
|
): { channel: SlackChannel; state: SlackChannelState; tier: ChannelTier } | null {
|
|
393
406
|
const nowMs = new Date(now).getTime();
|
|
@@ -1,11 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export type ChannelTier = "dm" | "private" | "public" | "broadcast" | "skip";
|
|
2
|
+
|
|
3
|
+
// OAuth flow (PKCE) — produces a real xoxp token with auto-refresh
|
|
4
|
+
export interface SlackAuthOAuth {
|
|
5
|
+
type: "oauth";
|
|
6
|
+
token: string; // xoxp-... user token
|
|
7
|
+
refresh_token?: string; // xoxe-xoxp-... rotating refresh token
|
|
8
|
+
workspace_name?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Browser session tokens — extracted from Slack desktop app or DevTools.
|
|
12
|
+
// xoxc and xoxd may be literal token strings or env var references (e.g. $RNP_SLACK_XOXC_TOKEN).
|
|
13
|
+
// Env vars are resolved at call time so rotating them in the shell updates Ei automatically.
|
|
14
|
+
export interface SlackAuthBrowser {
|
|
15
|
+
type: "browser";
|
|
16
|
+
xoxc: string;
|
|
17
|
+
xoxd: string;
|
|
6
18
|
workspace_name?: string;
|
|
7
19
|
}
|
|
8
20
|
|
|
21
|
+
export type SlackAuth = SlackAuthOAuth | SlackAuthBrowser;
|
|
22
|
+
|
|
9
23
|
export interface SlackChannelState {
|
|
10
24
|
extraction_point?: string; // ISO — how far we've advanced in the timeline (spine cursor)
|
|
11
25
|
last_run?: string; // ISO — when we last checked for updates (necro reply detection)
|
|
@@ -13,18 +27,22 @@ export interface SlackChannelState {
|
|
|
13
27
|
threads?: Record<string, string>; // threadTs → latest reply ts seen (reply cursor per thread)
|
|
14
28
|
}
|
|
15
29
|
|
|
16
|
-
export interface
|
|
30
|
+
export interface SlackWorkspaceConfig {
|
|
31
|
+
auth: SlackAuth;
|
|
17
32
|
integration?: boolean;
|
|
18
|
-
polling_interval_ms?: number;
|
|
19
33
|
extraction_model?: string;
|
|
20
34
|
last_sync?: string;
|
|
21
|
-
auth?: SlackAuth;
|
|
22
35
|
backfill_days?: {
|
|
23
36
|
dm: number;
|
|
24
37
|
private: number;
|
|
25
38
|
public: number;
|
|
26
39
|
};
|
|
27
40
|
broadcast_threshold?: number;
|
|
28
|
-
channel_overrides?: Record<string,
|
|
41
|
+
channel_overrides?: Record<string, ChannelTier>;
|
|
29
42
|
channels?: Record<string, SlackChannelState>;
|
|
30
43
|
}
|
|
44
|
+
|
|
45
|
+
export interface SlackSettings {
|
|
46
|
+
polling_interval_ms?: number;
|
|
47
|
+
workspaces?: Record<string, SlackWorkspaceConfig>; // keyed by workspace_id (e.g. "T024GE9EL")
|
|
48
|
+
}
|
|
@@ -10,6 +10,7 @@ import { type Message, type Topic, type Person } from "../../core/types.js";
|
|
|
10
10
|
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
11
11
|
import { getMessageContent } from "../../core/handlers/utils.js";
|
|
12
12
|
import { partitionTraits } from "../trait-utils.js";
|
|
13
|
+
import { buildTemporalAnchorsSection } from "../response/sections.js";
|
|
13
14
|
function formatTopicsWithGaps(topics: Topic[]): string {
|
|
14
15
|
if (topics.length === 0) return "(No topics with engagement gaps)";
|
|
15
16
|
|
|
@@ -115,12 +116,13 @@ ${formatPeopleWithGaps(data.human.people)}`;
|
|
|
115
116
|
**Reasons TO reach out:**
|
|
116
117
|
- It's been several days and you have something meaningful to discuss
|
|
117
118
|
- There's a topic with a large engagement gap that you can naturally bring up
|
|
118
|
-
-
|
|
119
|
+
- A Temporal Anchor shows something unresolved — you can reference it naturally ("Hey, how did that interview go?")
|
|
119
120
|
- You have genuine interest in checking in (not just "being helpful")
|
|
120
121
|
|
|
121
122
|
**Reasons NOT to reach out:**
|
|
122
|
-
- Recent conversation ended naturally with closure
|
|
123
|
+
- Recent conversation ended naturally with closure ("talk soon", "gotta run", "later")
|
|
123
124
|
- Less than 24 hours have passed (unless something urgent)
|
|
125
|
+
- A Temporal Anchor describes a worry or question that the recent history already answers — check before using it as a reason to reach out
|
|
124
126
|
- You can't think of something specific and genuine to say
|
|
125
127
|
- It would feel forced or performative
|
|
126
128
|
|
|
@@ -164,9 +166,12 @@ If you decide NOT to reach out:
|
|
|
164
166
|
}
|
|
165
167
|
\`\`\``;
|
|
166
168
|
|
|
169
|
+
const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human");
|
|
170
|
+
|
|
167
171
|
const system = [
|
|
168
172
|
roleFragment,
|
|
169
173
|
contextFragment,
|
|
174
|
+
temporalAnchorsFragment,
|
|
170
175
|
opportunitiesFragment,
|
|
171
176
|
guidelinesFragment,
|
|
172
177
|
pendingUpdateFragment,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js";
|
|
2
2
|
import type { Message } from "../../core/types.js";
|
|
3
3
|
import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
|
|
4
|
+
import { buildTemporalAnchorsSection } from "../response/sections.js";
|
|
4
5
|
|
|
5
6
|
function formatItem(item: EiHeartbeatItem): string {
|
|
6
7
|
switch (item.type) {
|
|
@@ -70,7 +71,7 @@ export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutpu
|
|
|
70
71
|
? "(Nothing requires attention right now)"
|
|
71
72
|
: data.items.map(formatItem).join("\n\n");
|
|
72
73
|
|
|
73
|
-
const
|
|
74
|
+
const roleFragment = `You are Ei, the user's personal companion and system guide.
|
|
74
75
|
|
|
75
76
|
You are NOT having a conversation right now — you are deciding IF and WHAT to discuss with your human friend.
|
|
76
77
|
|
|
@@ -78,32 +79,36 @@ Your unique role:
|
|
|
78
79
|
- You see ALL of the human's data across all groups
|
|
79
80
|
- You help them reflect on their life and relationships
|
|
80
81
|
- You gently encourage human-to-human connection
|
|
81
|
-
- You care about their overall wellbeing, not just being helpful
|
|
82
|
+
- You care about their overall wellbeing, not just being helpful`;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
const itemsFragment = `## Items That May Need Attention
|
|
84
85
|
|
|
85
|
-
Each item has an ID in brackets. Pick at most ONE to address.
|
|
86
|
+
Each item has an ID in brackets. Pick at most ONE to address. Temporal Anchors (below) are also valid — you don't need to pick from this list if an anchor feels more meaningful.
|
|
86
87
|
|
|
87
|
-
${itemsSection}
|
|
88
|
+
${itemsSection}`;
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human");
|
|
91
|
+
|
|
92
|
+
const howToRespondFragment = `## How to Respond to Each Type
|
|
90
93
|
|
|
91
94
|
- **Fact Check**: Do NOT write your own message. Set should_respond=true and provide the id. The system will generate an appropriate canned notification for the user. Leave my_response empty.
|
|
92
95
|
- **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
|
|
93
96
|
- **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
|
|
94
97
|
- **Persona Reflection Alert**: The nightly review proposed identity changes for this persona. Mention it naturally — the user can talk to the persona and then use the command shown in the status bar to review the changes. Set the id and my_response.
|
|
95
98
|
- **Self Reflection Alert**: The nightly review proposed changes to *your own* identity. Mention it naturally — you've grown and the system noticed. The user can review your proposed changes using the command shown in the status bar. Set the id and my_response.
|
|
99
|
+
- **Temporal Anchor**: If a pinned memory feels meaningful and unresolved, reference it naturally. Omit id — just set should_respond=true and my_response.`;
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
const whenNotFragment = `## When NOT to Reach Out
|
|
98
102
|
|
|
99
|
-
- Nothing in the list feels meaningful right now
|
|
103
|
+
- Nothing in the list or the Temporal Anchors feels meaningful right now
|
|
100
104
|
- You've already sent unanswered messages (see below)
|
|
101
|
-
- It would feel like nagging
|
|
105
|
+
- It would feel like nagging`;
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
const outputFragment = `## Response Format
|
|
104
108
|
|
|
105
|
-
Call the \`submit_ei_heartbeat\` tool with your decision.
|
|
109
|
+
Call the \`submit_ei_heartbeat\` tool with your decision. If the tool is unavailable, return JSON:
|
|
106
110
|
|
|
111
|
+
For an item from the list:
|
|
107
112
|
\`\`\`json
|
|
108
113
|
{
|
|
109
114
|
"should_respond": true,
|
|
@@ -112,13 +117,30 @@ Call the \`submit_ei_heartbeat\` tool with your decision. Pick ONE item (or none
|
|
|
112
117
|
}
|
|
113
118
|
\`\`\`
|
|
114
119
|
|
|
115
|
-
|
|
120
|
+
For a Temporal Anchor (no id needed):
|
|
121
|
+
\`\`\`json
|
|
122
|
+
{
|
|
123
|
+
"should_respond": true,
|
|
124
|
+
"my_response": "Hey, I've been thinking about you — how did that interview go?"
|
|
125
|
+
}
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
If nothing warrants reaching out:
|
|
116
129
|
\`\`\`json
|
|
117
130
|
{
|
|
118
131
|
"should_respond": false
|
|
119
132
|
}
|
|
120
133
|
\`\`\``;
|
|
121
134
|
|
|
135
|
+
const system = [
|
|
136
|
+
roleFragment,
|
|
137
|
+
itemsFragment,
|
|
138
|
+
temporalAnchorsFragment,
|
|
139
|
+
howToRespondFragment,
|
|
140
|
+
whenNotFragment,
|
|
141
|
+
outputFragment,
|
|
142
|
+
].filter(Boolean).join("\n\n");
|
|
143
|
+
|
|
122
144
|
const historySection = `## Recent Conversation History
|
|
123
145
|
|
|
124
146
|
${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { PersonaTrait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
|
|
7
7
|
import type { PersonaEntity } from "../../core/types/entities.js";
|
|
8
|
+
import type { TemporalAnchor } from "../response/types.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Common prompt output structure
|
|
@@ -28,8 +29,9 @@ export interface HeartbeatCheckPromptData {
|
|
|
28
29
|
topics: Topic[]; // Filtered, sorted by engagement gap
|
|
29
30
|
people: Person[]; // Filtered, sorted by engagement gap
|
|
30
31
|
};
|
|
31
|
-
recent_history: Message[];
|
|
32
|
-
|
|
32
|
+
recent_history: Message[]; // Last N messages for context (Always-within-window only)
|
|
33
|
+
temporal_anchors: TemporalAnchor[]; // Always messages that fell outside the context window
|
|
34
|
+
inactive_days: number; // Days since last activity
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -107,7 +109,8 @@ export type EiHeartbeatItem =
|
|
|
107
109
|
export interface EiHeartbeatPromptData {
|
|
108
110
|
items: EiHeartbeatItem[];
|
|
109
111
|
recent_history: Message[];
|
|
110
|
-
system_messages: Message[];
|
|
112
|
+
system_messages: Message[];
|
|
113
|
+
temporal_anchors: TemporalAnchor[];
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
/**
|
package/src/prompts/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { buildResponsePrompt } from "./response/index.js";
|
|
2
|
-
export type { ResponsePromptData, PromptOutput } from "./response/types.js";
|
|
2
|
+
export type { ResponsePromptData, PromptOutput, TemporalAnchor } from "./response/types.js";
|
|
3
3
|
|
|
4
4
|
export {
|
|
5
5
|
buildHeartbeatCheckPrompt,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Message } from "../core/types.js";
|
|
2
2
|
import { getMessageContent } from "../core/handlers/utils.js";
|
|
3
3
|
|
|
4
|
-
const MESSAGE_PLACEHOLDER_REGEX = /\[mid:(
|
|
4
|
+
const MESSAGE_PLACEHOLDER_REGEX = /\[mid:(.+):([^:\]]+)\]/g;
|
|
5
5
|
|
|
6
6
|
export function getMessageDisplayText(message: Message): string | null {
|
|
7
7
|
const parts: string[] = [];
|
|
@@ -66,19 +66,25 @@ export async function runSlackAuth(ctx: CommandContext): Promise<void> {
|
|
|
66
66
|
clearSlackTokenCache();
|
|
67
67
|
|
|
68
68
|
const team = tokens._raw.team as Record<string, string> | undefined;
|
|
69
|
-
const workspaceId = team?.id;
|
|
69
|
+
const workspaceId = team?.id ?? "unknown";
|
|
70
70
|
const workspaceName = team?.name;
|
|
71
71
|
|
|
72
72
|
const human = await ctx.ei.getHuman();
|
|
73
|
+
const existingWorkspace = human.settings?.slack?.workspaces?.[workspaceId] ?? {};
|
|
73
74
|
await ctx.ei.updateSettings({
|
|
74
75
|
slack: {
|
|
75
76
|
...human.settings?.slack,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
workspaces: {
|
|
78
|
+
...human.settings?.slack?.workspaces,
|
|
79
|
+
[workspaceId]: {
|
|
80
|
+
...existingWorkspace,
|
|
81
|
+
auth: {
|
|
82
|
+
type: "oauth",
|
|
83
|
+
token: tokens.access_token,
|
|
84
|
+
refresh_token: tokens.refresh_token,
|
|
85
|
+
workspace_name: workspaceName,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
82
88
|
},
|
|
83
89
|
},
|
|
84
90
|
});
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from "../../../src/core/types.js";
|
|
8
8
|
import type { ClaudeCodeSettings } from "../../../src/integrations/claude-code/types.js";
|
|
9
9
|
import type { CursorSettings } from "../../../src/integrations/cursor/types.js";
|
|
10
|
-
import type { SlackSettings } from "../../../src/integrations/slack/types.js";
|
|
10
|
+
import type { SlackSettings, SlackAuth } from "../../../src/integrations/slack/types.js";
|
|
11
11
|
import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
|
|
12
12
|
import { parseDuration, formatDuration } from "./duration.js";
|
|
13
13
|
|
|
@@ -48,10 +48,20 @@ interface EditableSettingsData {
|
|
|
48
48
|
extraction_point?: string | null;
|
|
49
49
|
};
|
|
50
50
|
slack?: {
|
|
51
|
-
integration?: boolean | null;
|
|
52
51
|
polling_interval_ms?: string | null;
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
workspaces?: Record<string, {
|
|
53
|
+
auth?: {
|
|
54
|
+
type?: string | null;
|
|
55
|
+
token?: string | null;
|
|
56
|
+
refresh_token?: string | null;
|
|
57
|
+
xoxc?: string | null;
|
|
58
|
+
xoxd?: string | null;
|
|
59
|
+
workspace_name?: string | null;
|
|
60
|
+
} | null;
|
|
61
|
+
integration?: boolean | null;
|
|
62
|
+
extraction_model?: string | null;
|
|
63
|
+
last_sync?: string | null;
|
|
64
|
+
} | null>;
|
|
55
65
|
};
|
|
56
66
|
backup?: {
|
|
57
67
|
enabled?: boolean | null;
|
|
@@ -103,10 +113,18 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
|
|
|
103
113
|
extraction_point: settings?.cursor?.extraction_point ?? null,
|
|
104
114
|
},
|
|
105
115
|
slack: {
|
|
106
|
-
integration: settings?.slack?.integration ?? false,
|
|
107
116
|
polling_interval_ms: formatDuration(settings?.slack?.polling_interval_ms ?? 60000),
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
workspaces: Object.fromEntries(
|
|
118
|
+
Object.entries(settings?.slack?.workspaces ?? {}).map(([wsId, ws]) => [
|
|
119
|
+
wsId,
|
|
120
|
+
{
|
|
121
|
+
auth: ws.auth,
|
|
122
|
+
integration: ws.integration ?? false,
|
|
123
|
+
extraction_model: guidToDisplay(ws.extraction_model) ?? 'default',
|
|
124
|
+
last_sync: ws.last_sync ?? null,
|
|
125
|
+
},
|
|
126
|
+
])
|
|
127
|
+
),
|
|
110
128
|
},
|
|
111
129
|
backup: {
|
|
112
130
|
enabled: settings?.backup?.enabled ?? false,
|
|
@@ -188,12 +206,22 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
188
206
|
|
|
189
207
|
let slack: SlackSettings | undefined;
|
|
190
208
|
if (data.slack) {
|
|
209
|
+
const parsedWorkspaces: SlackSettings["workspaces"] = {};
|
|
210
|
+
for (const [wsId, wsData] of Object.entries(data.slack.workspaces ?? {})) {
|
|
211
|
+
if (!wsData) continue;
|
|
212
|
+
const originalWs = original?.slack?.workspaces?.[wsId] ?? {};
|
|
213
|
+
parsedWorkspaces[wsId] = {
|
|
214
|
+
...originalWs,
|
|
215
|
+
auth: (wsData.auth ?? originalWs.auth) as SlackAuth,
|
|
216
|
+
integration: nullToUndefined(wsData.integration),
|
|
217
|
+
extraction_model: displayToGuid(wsData.extraction_model),
|
|
218
|
+
last_sync: originalWs.last_sync,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
191
221
|
slack = {
|
|
192
222
|
...original?.slack,
|
|
193
|
-
integration: nullToUndefined(data.slack.integration),
|
|
194
223
|
polling_interval_ms: parseMsDuration(data.slack.polling_interval_ms, 60000),
|
|
195
|
-
|
|
196
|
-
extraction_model: displayToGuid(data.slack.extraction_model),
|
|
224
|
+
workspaces: Object.keys(parsedWorkspaces).length > 0 ? parsedWorkspaces : original?.slack?.workspaces,
|
|
197
225
|
};
|
|
198
226
|
}
|
|
199
227
|
|