ei-tui 1.3.5 → 1.4.1
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/heartbeat-manager.ts +58 -4
- 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/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/human/person-scan.ts +16 -2
- package/src/prompts/human/types.ts +6 -0
- package/src/prompts/index.ts +1 -1
- 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
|
@@ -14,7 +14,7 @@ Your goal is to produce a well-structured markdown document that a human could s
|
|
|
14
14
|
|
|
15
15
|
Everything below is complete as provided — do not use tools to re-fetch records already present here. Only use tools to fill genuine gaps not covered by the data below.
|
|
16
16
|
|
|
17
|
-
- **Facts**:
|
|
17
|
+
- **Facts**: User demographics only (name, age, job title, location, family structure, physical traits) — not interests or opinions.
|
|
18
18
|
- **Topics**: Areas of interest, work, or concern with descriptions.
|
|
19
19
|
- **People**: Individuals with relationship context.
|
|
20
20
|
- **Quotes**: Verbatim things said, with a \`message_id\`. Use \`fetch_message\` with the \`message_id\` if you want the surrounding conversation for additional context.${hasEntityMap ? `
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const SLACK_PERSONA_DEFINITION = {
|
|
2
|
+
entity: "system" as const,
|
|
3
|
+
aliases: ["Slack", "slack"],
|
|
4
|
+
short_description: "Your Slack workspace — conversations, threads, and the people you work with, indexed for memory.",
|
|
5
|
+
long_description: `Slack is the quiet archivist of your working life. It reads your channels and threads, extracts what matters — who said what, what topics are alive, what decisions were made — and feeds that into Ei's memory so your personas know what's been going on without you having to explain it.
|
|
6
|
+
|
|
7
|
+
It doesn't chat. It doesn't have opinions. It's infrastructure with good taste about what's worth remembering.`,
|
|
8
|
+
model: undefined,
|
|
9
|
+
group_primary: "Integrations",
|
|
10
|
+
groups_visible: [] as string[],
|
|
11
|
+
traits: [],
|
|
12
|
+
topics: [],
|
|
13
|
+
heartbeat_delay_ms: 0,
|
|
14
|
+
is_archived: false,
|
|
15
|
+
is_paused: false,
|
|
16
|
+
is_static: true,
|
|
17
|
+
};
|
package/tui/README.md
CHANGED
|
@@ -41,6 +41,32 @@ Sessions are processed oldest-first, one per queue cycle. On first run Ei works
|
|
|
41
41
|
|
|
42
42
|
OpenCode also supports reading Ei's extracted knowledge back out via the [CLI tool](../src/cli/README.md), giving it persistent memory across sessions.
|
|
43
43
|
|
|
44
|
+
## Slack Integration
|
|
45
|
+
|
|
46
|
+
Slack is different from the coding tool integrations — it reads human conversations rather than coding sessions, and requires OAuth instead of local file access.
|
|
47
|
+
|
|
48
|
+
**Setup:**
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
/auth slack
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This opens a browser, walks you through OAuth, and stores your token. Then enable in `/settings`:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
slack:
|
|
58
|
+
integration: true
|
|
59
|
+
extraction_model: default # optional override
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Ei will index channels and DMs you're a member of, extracting topics, people, and context. Everything is processed locally — nothing is sent to the developer.
|
|
63
|
+
|
|
64
|
+
**Notes:**
|
|
65
|
+
- Ei uses a [published Slack app](https://github.com/Flare576/ei) — your workspace admin may need to approve it
|
|
66
|
+
- Non-Marketplace apps are subject to Slack's rate limits on `conversations.history` (1 req/min). Backfill is gradual; steady-state is fast.
|
|
67
|
+
- 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) — that link goes directly to the install flow
|
|
68
|
+
- The Slack app is read-only — Ei never posts, reacts, or takes any action in your workspace
|
|
69
|
+
|
|
44
70
|
# Installation
|
|
45
71
|
|
|
46
72
|
```bash
|
|
@@ -146,6 +172,7 @@ Rooms have three modes, set at creation time:
|
|
|
146
172
|
| `/settings` | `/set` | Edit your global settings in `$EDITOR` |
|
|
147
173
|
| `/setsync <user> <pass>` | `/ss` | Set sync credentials (triggers restart) |
|
|
148
174
|
| `/tools` | | Manage tool providers — enable/disable tools per persona |
|
|
175
|
+
| `/auth <service>` | | Authenticate with an external service via OAuth. Supported: `spotify`, `slack` |
|
|
149
176
|
|
|
150
177
|
### Editor
|
|
151
178
|
|
package/tui/src/commands/auth.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import type { Command } from "./registry.js";
|
|
2
2
|
import { runSpotifyAuth } from "./spotify-auth.js";
|
|
3
|
+
import { runSlackAuth } from "./slack-auth.js";
|
|
3
4
|
|
|
4
5
|
export const authCommand: Command = {
|
|
5
6
|
name: "auth",
|
|
6
7
|
aliases: [],
|
|
7
8
|
description: "Authenticate with a service (e.g. /auth spotify)",
|
|
8
|
-
usage: "/auth <service> — supported: spotify",
|
|
9
|
+
usage: "/auth <service> — supported: spotify, slack",
|
|
9
10
|
|
|
10
11
|
async execute(args, ctx) {
|
|
11
12
|
const service = args[0]?.toLowerCase();
|
|
12
13
|
|
|
13
14
|
if (!service) {
|
|
14
|
-
ctx.showNotification("Usage: /auth <service> (supported: spotify)", "error");
|
|
15
|
+
ctx.showNotification("Usage: /auth <service> (supported: spotify, slack)", "error");
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -19,8 +20,11 @@ export const authCommand: Command = {
|
|
|
19
20
|
case "spotify":
|
|
20
21
|
await runSpotifyAuth(ctx);
|
|
21
22
|
break;
|
|
23
|
+
case "slack":
|
|
24
|
+
await runSlackAuth(ctx);
|
|
25
|
+
break;
|
|
22
26
|
default:
|
|
23
|
-
ctx.showNotification(`Unknown service: ${service}. Supported: spotify`, "error");
|
|
27
|
+
ctx.showNotification(`Unknown service: ${service}. Supported: spotify, slack`, "error");
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { CommandContext } from "./registry.js";
|
|
2
|
+
import { logger } from "../util/logger.js";
|
|
3
|
+
import {
|
|
4
|
+
generateVerifier,
|
|
5
|
+
generateChallenge,
|
|
6
|
+
buildAuthUrl,
|
|
7
|
+
exchangeCode,
|
|
8
|
+
} from "../../../src/core/tools/builtin/pkce.js";
|
|
9
|
+
import {
|
|
10
|
+
SLACK_CLIENT_ID,
|
|
11
|
+
SLACK_USER_SCOPES,
|
|
12
|
+
SLACK_TUI_REDIRECT_URI,
|
|
13
|
+
SLACK_TUI_PORT,
|
|
14
|
+
clearSlackTokenCache,
|
|
15
|
+
} from "../../../src/core/tools/builtin/slack-auth.js";
|
|
16
|
+
|
|
17
|
+
export async function runSlackAuth(ctx: CommandContext): Promise<void> {
|
|
18
|
+
logger.info("[slack-auth] runSlackAuth() called");
|
|
19
|
+
ctx.showNotification("Starting Slack auth — opening browser…", "info");
|
|
20
|
+
|
|
21
|
+
const verifier = generateVerifier();
|
|
22
|
+
const challenge = await generateChallenge(verifier);
|
|
23
|
+
logger.info("[slack-auth] PKCE verifier + challenge generated");
|
|
24
|
+
|
|
25
|
+
const authUrl = buildAuthUrl({
|
|
26
|
+
clientId: SLACK_CLIENT_ID,
|
|
27
|
+
redirectUri: SLACK_TUI_REDIRECT_URI,
|
|
28
|
+
scopes: [],
|
|
29
|
+
userScopes: SLACK_USER_SCOPES,
|
|
30
|
+
challenge,
|
|
31
|
+
authEndpoint: "https://slack.com/oauth/v2/authorize",
|
|
32
|
+
});
|
|
33
|
+
logger.info("[slack-auth] Auth URL built", { redirectUri: SLACK_TUI_REDIRECT_URI });
|
|
34
|
+
|
|
35
|
+
const codePromise = waitForAuthCode(ctx);
|
|
36
|
+
|
|
37
|
+
const openCmd = process.platform === "darwin"
|
|
38
|
+
? "open"
|
|
39
|
+
: process.platform === "win32"
|
|
40
|
+
? "cmd /c start"
|
|
41
|
+
: "xdg-open";
|
|
42
|
+
|
|
43
|
+
logger.info("[slack-auth] Spawning browser", { openCmd });
|
|
44
|
+
Bun.spawn([openCmd, authUrl], { stdio: ["ignore", "ignore", "ignore"] });
|
|
45
|
+
logger.info("[slack-auth] Browser spawned — awaiting OAuth callback…");
|
|
46
|
+
|
|
47
|
+
const code = await codePromise;
|
|
48
|
+
logger.info("[slack-auth] codePromise resolved", { gotCode: !!code });
|
|
49
|
+
|
|
50
|
+
if (!code) return;
|
|
51
|
+
|
|
52
|
+
ctx.showNotification("Exchanging auth code for tokens…", "info");
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
logger.info("[slack-auth] Exchanging code for tokens");
|
|
56
|
+
const tokens = await exchangeCode({
|
|
57
|
+
code,
|
|
58
|
+
verifier,
|
|
59
|
+
redirectUri: SLACK_TUI_REDIRECT_URI,
|
|
60
|
+
clientId: SLACK_CLIENT_ID,
|
|
61
|
+
tokenEndpoint: "https://slack.com/api/oauth.v2.access",
|
|
62
|
+
tokenResponsePath: ["authed_user"],
|
|
63
|
+
});
|
|
64
|
+
logger.info("[slack-auth] Token exchange succeeded — storing tokens");
|
|
65
|
+
|
|
66
|
+
clearSlackTokenCache();
|
|
67
|
+
|
|
68
|
+
const team = tokens._raw.team as Record<string, string> | undefined;
|
|
69
|
+
const workspaceId = team?.id;
|
|
70
|
+
const workspaceName = team?.name;
|
|
71
|
+
|
|
72
|
+
const human = await ctx.ei.getHuman();
|
|
73
|
+
await ctx.ei.updateSettings({
|
|
74
|
+
slack: {
|
|
75
|
+
...human.settings?.slack,
|
|
76
|
+
auth: {
|
|
77
|
+
type: "pkce",
|
|
78
|
+
token: tokens.access_token,
|
|
79
|
+
refresh_token: tokens.refresh_token,
|
|
80
|
+
workspace_id: workspaceId,
|
|
81
|
+
workspace_name: workspaceName,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
logger.info("[slack-auth] Tokens stored — done!");
|
|
87
|
+
ctx.showNotification("✓ Slack connected successfully!", "info");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
logger.error("[slack-auth] Token exchange failed", { msg });
|
|
91
|
+
ctx.showNotification(`Slack auth failed: ${msg}`, "error");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function waitForAuthCode(ctx: CommandContext): Promise<string | null> {
|
|
96
|
+
return new Promise<string | null>((resolve) => {
|
|
97
|
+
const TIMEOUT_MS = 120_000;
|
|
98
|
+
|
|
99
|
+
let resolved = false;
|
|
100
|
+
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
101
|
+
|
|
102
|
+
const finish = (code: string | null) => {
|
|
103
|
+
if (resolved) return;
|
|
104
|
+
resolved = true;
|
|
105
|
+
logger.info("[slack-auth] finish() called", { gotCode: !!code });
|
|
106
|
+
try { server?.stop(true); } catch { /* ignore */ }
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolve(code);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
logger.warn("[slack-auth] Timed out waiting for callback");
|
|
113
|
+
ctx.showNotification("Slack auth timed out (2 min)", "error");
|
|
114
|
+
finish(null);
|
|
115
|
+
}, TIMEOUT_MS);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
server = Bun.serve({
|
|
119
|
+
port: SLACK_TUI_PORT,
|
|
120
|
+
hostname: "127.0.0.1",
|
|
121
|
+
fetch(req) {
|
|
122
|
+
const url = new URL(req.url);
|
|
123
|
+
logger.info("[slack-auth] Incoming request", { method: req.method, path: url.pathname });
|
|
124
|
+
|
|
125
|
+
if (url.pathname !== "/") {
|
|
126
|
+
return new Response("Not found", { status: 404 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const code = url.searchParams.get("code");
|
|
130
|
+
const error = url.searchParams.get("error");
|
|
131
|
+
logger.info("[slack-auth] Callback params", { hasCode: !!code, error });
|
|
132
|
+
|
|
133
|
+
if (error || !code) {
|
|
134
|
+
const msg = error ?? "no code in callback";
|
|
135
|
+
logger.error("[slack-auth] Auth denied or missing code", { msg });
|
|
136
|
+
ctx.showNotification(`Slack denied auth: ${msg}`, "error");
|
|
137
|
+
finish(null);
|
|
138
|
+
return new Response(
|
|
139
|
+
"<html><body><h2>Auth failed — return to your terminal.</h2></body></html>",
|
|
140
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const resp = new Response(
|
|
145
|
+
"<html><head><meta charset=\"utf-8\"></head><body><h2>✓ Slack connected! You can close this tab.</h2></body></html>",
|
|
146
|
+
{ headers: { "Content-Type": "text/html; charset=utf-8" } }
|
|
147
|
+
);
|
|
148
|
+
setTimeout(() => finish(code), 0);
|
|
149
|
+
return resp;
|
|
150
|
+
},
|
|
151
|
+
error(err) {
|
|
152
|
+
logger.error("[slack-auth] Bun.serve error", { msg: err.message });
|
|
153
|
+
ctx.showNotification(`Local auth server error: ${err.message}`, "error");
|
|
154
|
+
finish(null);
|
|
155
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
logger.info("[slack-auth] Bun.serve started", { port: server.port });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
+
logger.error("[slack-auth] Bun.serve failed to start", { msg });
|
|
162
|
+
ctx.showNotification(`Failed to start local auth server: ${msg}`, "error");
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
resolve(null);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
package/tui/src/util/logger.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** File-based logger for TUI debugging. Usage: tail -f $EI_DATA_PATH/tui.log */
|
|
2
2
|
|
|
3
|
-
import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync,
|
|
3
|
+
import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, copyFileSync, truncateSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { resolveDataPath } from "./resolve-data-path.js";
|
|
6
6
|
|
|
@@ -64,7 +64,8 @@ export function rotateLog(): void {
|
|
|
64
64
|
|
|
65
65
|
if (existsSync(logPath)) {
|
|
66
66
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
|
|
67
|
-
|
|
67
|
+
copyFileSync(logPath, join(dataDir, `tui-${ts}.log`));
|
|
68
|
+
truncateSync(logPath, 0);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
const rolled = readdirSync(dataDir)
|
|
@@ -7,6 +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
11
|
import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
|
|
11
12
|
import { parseDuration, formatDuration } from "./duration.js";
|
|
12
13
|
|
|
@@ -46,6 +47,12 @@ interface EditableSettingsData {
|
|
|
46
47
|
last_sync?: string | null;
|
|
47
48
|
extraction_point?: string | null;
|
|
48
49
|
};
|
|
50
|
+
slack?: {
|
|
51
|
+
integration?: boolean | null;
|
|
52
|
+
polling_interval_ms?: string | null;
|
|
53
|
+
last_sync?: string | null;
|
|
54
|
+
extraction_model?: string | null;
|
|
55
|
+
};
|
|
49
56
|
backup?: {
|
|
50
57
|
enabled?: boolean | null;
|
|
51
58
|
max_backups?: number | null;
|
|
@@ -95,6 +102,12 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
|
|
|
95
102
|
last_sync: settings?.cursor?.last_sync ?? null,
|
|
96
103
|
extraction_point: settings?.cursor?.extraction_point ?? null,
|
|
97
104
|
},
|
|
105
|
+
slack: {
|
|
106
|
+
integration: settings?.slack?.integration ?? false,
|
|
107
|
+
polling_interval_ms: formatDuration(settings?.slack?.polling_interval_ms ?? 60000),
|
|
108
|
+
last_sync: settings?.slack?.last_sync ?? null,
|
|
109
|
+
extraction_model: guidToDisplay(settings?.slack?.extraction_model) ?? 'default',
|
|
110
|
+
},
|
|
98
111
|
backup: {
|
|
99
112
|
enabled: settings?.backup?.enabled ?? false,
|
|
100
113
|
max_backups: settings?.backup?.max_backups ?? 24,
|
|
@@ -173,6 +186,17 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
173
186
|
};
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
let slack: SlackSettings | undefined;
|
|
190
|
+
if (data.slack) {
|
|
191
|
+
slack = {
|
|
192
|
+
...original?.slack,
|
|
193
|
+
integration: nullToUndefined(data.slack.integration),
|
|
194
|
+
polling_interval_ms: parseMsDuration(data.slack.polling_interval_ms, 60000),
|
|
195
|
+
last_sync: original?.slack?.last_sync,
|
|
196
|
+
extraction_model: displayToGuid(data.slack.extraction_model),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
176
200
|
let backup: import('../../../src/core/types.js').BackupConfig | undefined;
|
|
177
201
|
if (data.backup) {
|
|
178
202
|
backup = {
|
|
@@ -197,6 +221,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
197
221
|
opencode,
|
|
198
222
|
claudeCode,
|
|
199
223
|
cursor,
|
|
224
|
+
slack,
|
|
200
225
|
backup,
|
|
201
226
|
};
|
|
202
227
|
}
|