ei-tui 1.3.5 → 1.4.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/README.md +22 -0
- package/package.json +1 -1
- package/src/cli/mcp.ts +2 -2
- package/src/core/orchestrators/human-extraction.ts +2 -0
- package/src/core/processor.ts +61 -0
- package/src/core/tools/builtin/pkce.ts +40 -23
- package/src/core/tools/builtin/slack-auth.ts +117 -0
- package/src/core/tools/index.ts +1 -1
- package/src/core/types/entities.ts +1 -0
- package/src/core/utils/message-id.ts +4 -0
- package/src/integrations/slack/importer.ts +408 -0
- package/src/integrations/slack/reader.ts +416 -0
- package/src/integrations/slack/types.ts +30 -0
- package/src/prompts/human/person-scan.ts +16 -2
- package/src/prompts/human/types.ts +6 -0
- package/src/prompts/response/sections.ts +1 -1
- package/src/prompts/synthesis/index.ts +1 -1
- package/src/templates/slack.ts +17 -0
- package/tui/README.md +27 -0
- package/tui/src/commands/auth.ts +7 -3
- package/tui/src/commands/slack-auth.ts +167 -0
- package/tui/src/util/help-content.ts +1 -0
- package/tui/src/util/logger.ts +3 -2
- package/tui/src/util/yaml-settings.ts +25 -0
package/README.md
CHANGED
|
@@ -185,6 +185,28 @@ Ei splits the document into segments, runs them through the extraction pipeline,
|
|
|
185
185
|
|
|
186
186
|
Both surfaces show you which documents have been imported and let you remove their extracted knowledge (web: Delete button in the Documents tab; TUI: `/unsource <source_tag>`).
|
|
187
187
|
|
|
188
|
+
## Slack Integration
|
|
189
|
+
|
|
190
|
+
Ei can index your Slack workspace — channels, DMs, and threads — and extract the same topics, people, and context it pulls from your conversations. Your personas end up knowing what's been going on at work without you having to explain it.
|
|
191
|
+
|
|
192
|
+
**TUI** (requires setup):
|
|
193
|
+
```bash
|
|
194
|
+
/auth slack
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Opens a browser OAuth flow. Once authenticated, enable in `/settings`:
|
|
198
|
+
|
|
199
|
+
```yaml
|
|
200
|
+
slack:
|
|
201
|
+
integration: true
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Web**: Open **☰ menu** → **My Data** → **External** tab → **Connect Slack**.
|
|
205
|
+
|
|
206
|
+
Ei is read-only — it never posts, reacts, or takes any action in your workspace. All processing happens locally. Your workspace admin may need to approve the [Ei Slack app](https://slack.com/oauth/v2/authorize?client_id=11080256060354.11080294064034&scope=&user_scope=channels:history,channels:read,groups:history,groups:read,im:history,im:read,mpim:history,mpim:read,users:read,users:read.email) before you can connect.
|
|
207
|
+
|
|
208
|
+
> **Note on backfill speed**: Slack limits non-Marketplace apps to 1 request/minute on message history. Backfill through a large workspace is gradual — steady-state (indexing new messages) is fast once you're caught up.
|
|
209
|
+
|
|
188
210
|
## Knowledge Share
|
|
189
211
|
|
|
190
212
|
Sometimes you want to take what Ei knows and turn it into something you can hand to another human. A new teammate joining a project. A briefing doc before a meeting. A brain dump before a vacation.
|
package/package.json
CHANGED
package/src/cli/mcp.ts
CHANGED
|
@@ -134,7 +134,7 @@ export function createMcpServer(): McpServer {
|
|
|
134
134
|
"ei_find_memory",
|
|
135
135
|
{
|
|
136
136
|
description:
|
|
137
|
-
"Search Ei's persistent knowledge base — facts, topics, people, and quotes learned across ALL conversations over time. Use when you need context about the user, their life, relationships, or interests that may not be visible in the current exchange. Returns results grouped by type. Use `recent: true` to retrieve what's been discussed recently.",
|
|
137
|
+
"Search Ei's persistent knowledge base — facts, topics, people, and quotes learned across ALL conversations over time. Use when you need context about the user, their life, relationships, or interests that may not be visible in the current exchange. Returns results grouped by type. Use `recent: true` to retrieve what's been discussed recently. TYPE GUIDANCE: 'facts' are ONLY user demographics — name, age, job title, location, family structure, physical traits. For interests, opinions, hobbies, or anything the human cares about, use 'topics'. For named individuals, use 'people'. For verbatim things said, use 'quotes'.",
|
|
138
138
|
inputSchema: {
|
|
139
139
|
query: z
|
|
140
140
|
.string()
|
|
@@ -145,7 +145,7 @@ export function createMcpServer(): McpServer {
|
|
|
145
145
|
types: z
|
|
146
146
|
.array(z.enum(["facts", "topics", "people", "quotes"]))
|
|
147
147
|
.optional()
|
|
148
|
-
.describe("Limit search to specific memory types (default: all types)"),
|
|
148
|
+
.describe("Limit search to specific memory types (default: all types). Use 'facts' ONLY for user demographics (name, age, job, location, family). Use 'topics' for interests, opinions, and anything the human cares about."),
|
|
149
149
|
limit: z
|
|
150
150
|
.number()
|
|
151
151
|
.optional()
|
|
@@ -61,6 +61,7 @@ export interface ExtractionContext {
|
|
|
61
61
|
extraction_flag?: "f" | "t" | "p" | "e";
|
|
62
62
|
roomId?: string;
|
|
63
63
|
sources?: string[];
|
|
64
|
+
excluded_participants?: import("../../prompts/human/types.js").ExcludedParticipant[];
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface ExtractionOptions {
|
|
@@ -224,6 +225,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
|
|
|
224
225
|
messages_analyze: chunk.messages_analyze,
|
|
225
226
|
participant_context: buildParticipantContext(context.personaId, state),
|
|
226
227
|
known_identifier_types: userIdentifierTypesForScan,
|
|
228
|
+
excluded_participants: context.excluded_participants,
|
|
227
229
|
});
|
|
228
230
|
|
|
229
231
|
state.queue_enqueue({
|
package/src/core/processor.ts
CHANGED
|
@@ -169,6 +169,8 @@ export class Processor {
|
|
|
169
169
|
private claudeCodeImportInProgress = false;
|
|
170
170
|
private lastCursorSync = 0;
|
|
171
171
|
private cursorImportInProgress = false;
|
|
172
|
+
private lastSlackSync = 0;
|
|
173
|
+
private slackImportInProgress = false;
|
|
172
174
|
private pendingConflict: StateConflictData | null = null;
|
|
173
175
|
private storage: Storage | null = null;
|
|
174
176
|
private importAbortController = new AbortController();
|
|
@@ -1224,6 +1226,10 @@ export class Processor {
|
|
|
1224
1226
|
console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
|
|
1225
1227
|
this.claudeCodeImportInProgress = false;
|
|
1226
1228
|
}
|
|
1229
|
+
if (this.slackImportInProgress) {
|
|
1230
|
+
console.log(`[Processor ${this.instanceId}] Clearing slackImportInProgress flag`);
|
|
1231
|
+
this.slackImportInProgress = false;
|
|
1232
|
+
}
|
|
1227
1233
|
await this.stateManager.flush();
|
|
1228
1234
|
console.log(`[Processor ${this.instanceId}] pause() complete (main loop stopped, state flushed)`);
|
|
1229
1235
|
}
|
|
@@ -1474,6 +1480,15 @@ const toolNextSteps = new Set([
|
|
|
1474
1480
|
await this.checkAndSyncPersonaHistory(human);
|
|
1475
1481
|
}
|
|
1476
1482
|
|
|
1483
|
+
if (
|
|
1484
|
+
this.isTUI &&
|
|
1485
|
+
human.settings?.slack?.integration &&
|
|
1486
|
+
human.settings?.slack?.auth?.token &&
|
|
1487
|
+
this.stateManager.queue_length() === 0
|
|
1488
|
+
) {
|
|
1489
|
+
await this.checkAndSyncSlack(human, now);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1477
1492
|
if (human.settings?.ceremony && shouldStartCeremony(human.settings.ceremony, this.stateManager)) {
|
|
1478
1493
|
if (human.settings?.sync && remoteSync.isConfigured()) {
|
|
1479
1494
|
const state = this.stateManager.getStorageState();
|
|
@@ -1714,6 +1729,52 @@ const toolNextSteps = new Set([
|
|
|
1714
1729
|
});
|
|
1715
1730
|
}
|
|
1716
1731
|
|
|
1732
|
+
private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
|
|
1733
|
+
if (this.slackImportInProgress) return;
|
|
1734
|
+
|
|
1735
|
+
const slack = human.settings?.slack;
|
|
1736
|
+
const pollingInterval = slack?.polling_interval_ms ?? 60_000;
|
|
1737
|
+
const lastSync = slack?.last_sync ? new Date(slack.last_sync).getTime() : 0;
|
|
1738
|
+
|
|
1739
|
+
if (now - lastSync < pollingInterval && this.lastSlackSync > 0) return;
|
|
1740
|
+
|
|
1741
|
+
this.lastSlackSync = now;
|
|
1742
|
+
this.stateManager.setHuman({
|
|
1743
|
+
...this.stateManager.getHuman(),
|
|
1744
|
+
settings: {
|
|
1745
|
+
...this.stateManager.getHuman().settings,
|
|
1746
|
+
slack: { ...slack, last_sync: new Date(now).toISOString() },
|
|
1747
|
+
},
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
this.slackImportInProgress = true;
|
|
1751
|
+
import("../integrations/slack/importer.js")
|
|
1752
|
+
.then(({ importSlackChannel }) =>
|
|
1753
|
+
importSlackChannel({
|
|
1754
|
+
stateManager: this.stateManager,
|
|
1755
|
+
interface: this.interface,
|
|
1756
|
+
signal: this.importAbortController.signal,
|
|
1757
|
+
})
|
|
1758
|
+
)
|
|
1759
|
+
.then((result) => {
|
|
1760
|
+
if (result.channelProcessed) {
|
|
1761
|
+
console.log(
|
|
1762
|
+
`[Processor] Slack sync: #${result.channelProcessed} — ` +
|
|
1763
|
+
`${result.messagesImported} messages, ${result.threadsProcessed} threads, ` +
|
|
1764
|
+
`${result.scansQueued} scans queued`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
})
|
|
1768
|
+
.catch((err) => {
|
|
1769
|
+
const msg = err instanceof Error ? err.message : JSON.stringify(err);
|
|
1770
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
1771
|
+
console.warn(`[Processor] Slack sync failed: ${msg}${stack ? `\n${stack}` : ''}`);
|
|
1772
|
+
})
|
|
1773
|
+
.finally(() => {
|
|
1774
|
+
this.slackImportInProgress = false;
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1717
1778
|
private personaHistoryImportInProgress = false;
|
|
1718
1779
|
|
|
1719
1780
|
private async checkAndSyncPersonaHistory(_human: HumanEntity): Promise<void> {
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PKCE helpers — shared by Web
|
|
2
|
+
* PKCE helpers — shared by Web and TUI auth flows (Spotify, Slack, etc.).
|
|
3
3
|
*
|
|
4
4
|
* Uses the Web Crypto API (available in both browser and Bun/Node >= 19).
|
|
5
5
|
* All functions are synchronous except generateChallenge which needs crypto.subtle.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** Generate a random code verifier (128 chars, URL-safe base64). */
|
|
9
8
|
export function generateVerifier(): string {
|
|
10
9
|
const array = new Uint8Array(96); // 96 bytes → 128 chars base64url
|
|
11
10
|
crypto.getRandomValues(array);
|
|
12
11
|
return base64url(array);
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/** Derive the PKCE code challenge (SHA-256 of verifier, base64url). */
|
|
16
14
|
export async function generateChallenge(verifier: string): Promise<string> {
|
|
17
15
|
const encoder = new TextEncoder();
|
|
18
16
|
const data = encoder.encode(verifier);
|
|
@@ -20,14 +18,22 @@ export async function generateChallenge(verifier: string): Promise<string> {
|
|
|
20
18
|
return base64url(new Uint8Array(digest));
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
/** Exchange an authorization code for tokens (used by both Web and TUI). */
|
|
24
21
|
export async function exchangeCode(params: {
|
|
25
22
|
code: string;
|
|
26
23
|
verifier: string;
|
|
27
24
|
redirectUri: string;
|
|
28
25
|
clientId: string;
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
tokenEndpoint?: string;
|
|
27
|
+
tokenResponsePath?: string[];
|
|
28
|
+
}): Promise<{ access_token: string; refresh_token: string; expires_in: number; _raw: Record<string, unknown> }> {
|
|
29
|
+
const {
|
|
30
|
+
code,
|
|
31
|
+
verifier,
|
|
32
|
+
redirectUri,
|
|
33
|
+
clientId,
|
|
34
|
+
tokenEndpoint = "https://accounts.spotify.com/api/token",
|
|
35
|
+
tokenResponsePath,
|
|
36
|
+
} = params;
|
|
31
37
|
|
|
32
38
|
const body = new URLSearchParams({
|
|
33
39
|
grant_type: "authorization_code",
|
|
@@ -37,7 +43,7 @@ export async function exchangeCode(params: {
|
|
|
37
43
|
code_verifier: verifier,
|
|
38
44
|
});
|
|
39
45
|
|
|
40
|
-
const response = await fetch(
|
|
46
|
+
const response = await fetch(tokenEndpoint, {
|
|
41
47
|
method: "POST",
|
|
42
48
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
43
49
|
body: body.toString(),
|
|
@@ -45,47 +51,58 @@ export async function exchangeCode(params: {
|
|
|
45
51
|
|
|
46
52
|
if (!response.ok) {
|
|
47
53
|
const text = await response.text();
|
|
48
|
-
throw new Error(`
|
|
54
|
+
throw new Error(`Token exchange failed (${response.status}): ${text}`);
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const json = await response.json() as Record<string, unknown>;
|
|
58
|
+
|
|
59
|
+
const payload = tokenResponsePath
|
|
60
|
+
? tokenResponsePath.reduce<Record<string, unknown>>((obj, key) => {
|
|
61
|
+
const next = obj[key];
|
|
62
|
+
return (next && typeof next === "object" ? next : obj) as Record<string, unknown>;
|
|
63
|
+
}, json)
|
|
64
|
+
: json;
|
|
65
|
+
|
|
66
|
+
return { ...(payload as { access_token: string; refresh_token: string; expires_in: number }), _raw: json };
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
/** Build the Spotify authorization URL. */
|
|
59
69
|
export function buildAuthUrl(params: {
|
|
60
70
|
clientId: string;
|
|
61
71
|
redirectUri: string;
|
|
62
72
|
scopes: string[];
|
|
63
73
|
challenge: string;
|
|
64
74
|
state?: string;
|
|
75
|
+
userScopes?: string[];
|
|
76
|
+
authEndpoint?: string;
|
|
65
77
|
}): string {
|
|
66
|
-
const {
|
|
67
|
-
|
|
78
|
+
const {
|
|
79
|
+
clientId,
|
|
80
|
+
redirectUri,
|
|
81
|
+
scopes,
|
|
82
|
+
challenge,
|
|
83
|
+
state,
|
|
84
|
+
userScopes,
|
|
85
|
+
authEndpoint = "https://accounts.spotify.com/authorize",
|
|
86
|
+
} = params;
|
|
87
|
+
|
|
88
|
+
const url = new URL(authEndpoint);
|
|
68
89
|
url.searchParams.set("client_id", clientId);
|
|
69
90
|
url.searchParams.set("response_type", "code");
|
|
70
91
|
url.searchParams.set("redirect_uri", redirectUri);
|
|
71
|
-
url.searchParams.set("scope", scopes.join(" "));
|
|
92
|
+
if (scopes.length > 0) url.searchParams.set("scope", scopes.join(" "));
|
|
93
|
+
if (userScopes && userScopes.length > 0) url.searchParams.set("user_scope", userScopes.join(" "));
|
|
72
94
|
url.searchParams.set("code_challenge", challenge);
|
|
73
95
|
url.searchParams.set("code_challenge_method", "S256");
|
|
74
96
|
if (state) url.searchParams.set("state", state);
|
|
75
97
|
return url.toString();
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Internal helpers
|
|
80
100
|
// ---------------------------------------------------------------------------
|
|
81
101
|
|
|
82
102
|
function base64url(buffer: Uint8Array): string {
|
|
83
|
-
// btoa works in browser; Buffer works in Node/Bun
|
|
84
|
-
let binary: string;
|
|
85
103
|
if (typeof btoa === "function") {
|
|
86
|
-
binary = String.fromCharCode(...buffer);
|
|
104
|
+
const binary = String.fromCharCode(...buffer);
|
|
87
105
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
88
106
|
}
|
|
89
|
-
// Node/Bun fallback
|
|
90
107
|
return Buffer.from(buffer).toString("base64url");
|
|
91
108
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack token refresh — shared helper for the Slack integration.
|
|
3
|
+
*
|
|
4
|
+
* Caches the current access token in module scope so multiple API calls
|
|
5
|
+
* within the same session don't each trigger a refresh round-trip.
|
|
6
|
+
*
|
|
7
|
+
* The refresh token is read from human.settings.slack.auth at call time,
|
|
8
|
+
* so it always reflects the latest stored value.
|
|
9
|
+
*
|
|
10
|
+
* Auth flow uses PKCE (no client_secret required — public client).
|
|
11
|
+
* Slack issues rotating refresh tokens for localhost/desktop redirects;
|
|
12
|
+
* callers must persist the new refresh token via onTokenRotated.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const SLACK_CLIENT_ID = "11080256060354.11080294064034";
|
|
16
|
+
|
|
17
|
+
export const SLACK_USER_SCOPES = [
|
|
18
|
+
"channels:history",
|
|
19
|
+
"channels:read",
|
|
20
|
+
"groups:history",
|
|
21
|
+
"groups:read",
|
|
22
|
+
"im:history",
|
|
23
|
+
"im:read",
|
|
24
|
+
"mpim:history",
|
|
25
|
+
"mpim:read",
|
|
26
|
+
"users:read",
|
|
27
|
+
"users:read.email",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// TUI redirect URI — Slack requires HTTPS for distributed apps, so we relay
|
|
31
|
+
// through ei.flare576.com/callback/slack/tui which does a 302 to localhost.
|
|
32
|
+
export const SLACK_TUI_REDIRECT_URI = "https://ei.flare576.com/callback/slack/tui";
|
|
33
|
+
export const SLACK_TUI_PORT = 4243;
|
|
34
|
+
|
|
35
|
+
// Web redirect URI — must match slack_manifest.yaml
|
|
36
|
+
export const SLACK_WEB_REDIRECT_URI = "https://ei.flare576.com/callback/slack";
|
|
37
|
+
|
|
38
|
+
interface CachedToken {
|
|
39
|
+
token: string;
|
|
40
|
+
expires_at: number; // Date.now() ms
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let cachedToken: CachedToken | null = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a valid Slack access token, refreshing if needed.
|
|
47
|
+
* @param refreshToken - The stored refresh token from human.settings.slack.auth
|
|
48
|
+
* @param onTokenRotated - Called with the new refresh token when Slack rotates it
|
|
49
|
+
*/
|
|
50
|
+
export async function getSlackAccessToken(
|
|
51
|
+
refreshToken: string,
|
|
52
|
+
onTokenRotated?: (newRefreshToken: string) => void
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
// Return cached token if still valid (60s buffer)
|
|
55
|
+
if (cachedToken && Date.now() < cachedToken.expires_at - 60_000) {
|
|
56
|
+
return cachedToken.token;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const body = new URLSearchParams({
|
|
60
|
+
grant_type: "refresh_token",
|
|
61
|
+
refresh_token: refreshToken,
|
|
62
|
+
client_id: SLACK_CLIENT_ID,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const response = await fetch("https://slack.com/api/oauth.v2.access", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
68
|
+
body: body.toString(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const text = await response.text();
|
|
73
|
+
throw new Error(`Slack token refresh failed (${response.status}): ${text}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data = (await response.json()) as {
|
|
77
|
+
ok: boolean;
|
|
78
|
+
error?: string;
|
|
79
|
+
authed_user?: {
|
|
80
|
+
access_token: string;
|
|
81
|
+
refresh_token?: string;
|
|
82
|
+
expires_in?: number;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (!data.ok || !data.authed_user?.access_token) {
|
|
87
|
+
throw new Error(`Slack token refresh failed: ${data.error ?? "unknown error"}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const expiresIn = data.authed_user.expires_in ?? 43200; // 12h default
|
|
91
|
+
cachedToken = {
|
|
92
|
+
token: data.authed_user.access_token,
|
|
93
|
+
expires_at: Date.now() + expiresIn * 1000,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Slack rotates the refresh token — persist the new one if provided
|
|
97
|
+
if (data.authed_user.refresh_token && data.authed_user.refresh_token !== refreshToken) {
|
|
98
|
+
onTokenRotated?.(data.authed_user.refresh_token);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return cachedToken.token;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Clear the cached token (call if refresh token changes). */
|
|
105
|
+
export function clearSlackTokenCache(): void {
|
|
106
|
+
cachedToken = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Return a structured "not authenticated" error. */
|
|
110
|
+
export function slackNotAuthenticatedError(): string {
|
|
111
|
+
return JSON.stringify({
|
|
112
|
+
error: "not_authenticated",
|
|
113
|
+
integration: "slack",
|
|
114
|
+
message:
|
|
115
|
+
"Slack is not connected. In the TUI: /auth slack. In the web app: My Data → External → Connect Slack.",
|
|
116
|
+
});
|
|
117
|
+
}
|
package/src/core/tools/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const SYSTEM_TOOLS: ToolDefinition[] = [
|
|
|
29
29
|
provider_id: "ei",
|
|
30
30
|
name: "find_memory",
|
|
31
31
|
display_name: "Find Memory",
|
|
32
|
-
description: "Semantic search of your personal memory — facts, topics, people, and quotes learned across ALL conversations over time, not just this one. Use when the human references something from the past, mentions a person, or asks about a topic you might have learned about. People and topic results include a sentiment field (e.g. '72% positive', 'neutral', '45% slightly negative') indicating how the human generally feels about that person or subject. Supports optional filters: types (array of 'facts', 'topics', 'people', 'quotes'), limit (1-20, default 10), recent (true = sort by recency), persona (filter to what a specific persona has learned — use display name).",
|
|
32
|
+
description: "Semantic search of your personal memory — facts, topics, people, and quotes learned across ALL conversations over time, not just this one. Use when the human references something from the past, mentions a person, or asks about a topic you might have learned about. People and topic results include a sentiment field (e.g. '72% positive', 'neutral', '45% slightly negative') indicating how the human generally feels about that person or subject. Supports optional filters: types (array of 'facts', 'topics', 'people', 'quotes'), limit (1-20, default 10), recent (true = sort by recency), persona (filter to what a specific persona has learned — use display name). TYPE GUIDANCE: 'facts' are ONLY user demographics — name, age, job title, location, family structure, physical traits. For interests, opinions, hobbies, or anything the human cares about, use 'topics'. For named individuals, use 'people'. For verbatim things said, use 'quotes'.",
|
|
33
33
|
input_schema: {
|
|
34
34
|
type: "object",
|
|
35
35
|
properties: {
|
|
@@ -132,6 +132,7 @@ export interface HumanSettings {
|
|
|
132
132
|
active_theme?: string;
|
|
133
133
|
custom_themes?: ThemeDefinition[];
|
|
134
134
|
personaHistory?: import("../../integrations/persona-history/types.js").PersonaHistorySettings;
|
|
135
|
+
slack?: import("../../integrations/slack/types.js").SlackSettings;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export interface HumanEntity {
|
|
@@ -112,3 +112,7 @@ export function qualifyCursorMessage(machine: string, sessionId: string, nativeI
|
|
|
112
112
|
export function qualifyDocumentMessage(slug: string, uuid: string): string {
|
|
113
113
|
return `import:document:${slug}:${uuid}`
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
export function qualifySlackMessage(workspaceId: string, channelId: string, ts: string): string {
|
|
117
|
+
return `slack:${workspaceId}:${channelId}:${ts}`
|
|
118
|
+
}
|