@hmawla/co-assistant 1.0.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/LICENSE +21 -0
- package/README.md +396 -0
- package/config.json.example +32 -0
- package/dist/cli/index.js +4547 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.js +3258 -0
- package/dist/index.js.map +1 -0
- package/heartbeats/email-reply-check.heartbeat.md +39 -0
- package/package.json +79 -0
- package/personality.md +48 -0
- package/plugins/gmail/README.md +78 -0
- package/plugins/gmail/auth.ts +92 -0
- package/plugins/gmail/index.ts +66 -0
- package/plugins/gmail/plugin.json +13 -0
- package/plugins/gmail/tools.ts +336 -0
- package/plugins/google-calendar/README.md +51 -0
- package/plugins/google-calendar/auth.ts +92 -0
- package/plugins/google-calendar/index.ts +82 -0
- package/plugins/google-calendar/plugin.json +13 -0
- package/plugins/google-calendar/tools.ts +328 -0
- package/user.md.example +38 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
You are checking my recent emails for any that require a reply from me.
|
|
2
|
+
|
|
3
|
+
## Instructions
|
|
4
|
+
|
|
5
|
+
1. Use the `gmail_search_emails` tool to fetch my last 5 emails (query: `in:inbox`, maxResults: 5).
|
|
6
|
+
2. For each email, use `gmail_read_email` to read its full content.
|
|
7
|
+
3. Skip any email whose message ID appears in the deduplication list below.
|
|
8
|
+
4. For each **new** email, determine whether it requires a reply from me. Consider:
|
|
9
|
+
- Direct questions asked to me
|
|
10
|
+
- Action items or requests directed at me
|
|
11
|
+
- Invitations or RSVPs awaiting my response
|
|
12
|
+
- Important threads where I'm expected to respond
|
|
13
|
+
- Do NOT flag: newsletters, automated notifications, marketing, no-reply senders, receipts
|
|
14
|
+
5. For each email that needs a reply, suggest a concise, professional reply draft.
|
|
15
|
+
|
|
16
|
+
## Output Format
|
|
17
|
+
|
|
18
|
+
For each email that needs a reply, format your output like this:
|
|
19
|
+
|
|
20
|
+
**📧 From:** [sender]
|
|
21
|
+
**Subject:** [subject]
|
|
22
|
+
**Why reply:** [brief reason]
|
|
23
|
+
**Suggested reply:**
|
|
24
|
+
> [your suggested reply text]
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
If no emails require a reply, do not say anything unless invoked with /heartbeat
|
|
29
|
+
|
|
30
|
+
## Deduplication
|
|
31
|
+
|
|
32
|
+
{{DEDUP_STATE}}
|
|
33
|
+
|
|
34
|
+
## IMPORTANT — Deduplication Marker
|
|
35
|
+
|
|
36
|
+
At the very end of your response, you MUST output exactly one line in this format with ALL email message IDs you checked (whether they needed a reply or not). This prevents re-checking the same emails next time:
|
|
37
|
+
|
|
38
|
+
<!-- PROCESSED: msg_id_1, msg_id_2, msg_id_3, msg_id_4, msg_id_5 -->
|
|
39
|
+
Also, make sure to not output the same message id multiple times, so if it exist don't push it.
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hmawla/co-assistant",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered Telegram personal assistant using GitHub Copilot SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./cli": "./dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"co-assistant": "dist/cli/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"plugins/",
|
|
17
|
+
"heartbeats/*.heartbeat.md",
|
|
18
|
+
"personality.md",
|
|
19
|
+
"user.md.example",
|
|
20
|
+
"config.json.example",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"dev": "tsx src/cli/index.ts",
|
|
27
|
+
"start": "node dist/index.js",
|
|
28
|
+
"cli": "tsx src/cli/index.ts",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"prepack": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"ai",
|
|
34
|
+
"assistant",
|
|
35
|
+
"telegram",
|
|
36
|
+
"bot",
|
|
37
|
+
"copilot",
|
|
38
|
+
"github-copilot",
|
|
39
|
+
"copilot-sdk",
|
|
40
|
+
"chatbot",
|
|
41
|
+
"plugins",
|
|
42
|
+
"gmail",
|
|
43
|
+
"google-calendar",
|
|
44
|
+
"automation"
|
|
45
|
+
],
|
|
46
|
+
"author": {
|
|
47
|
+
"name": "Hussein Al Mawla",
|
|
48
|
+
"email": "hussein@kockatoos.co"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/hmawla/co-assistant.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/hmawla/co-assistant/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/hmawla/co-assistant#readme",
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=20.0.0"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@github/copilot-sdk": "^0.2.1",
|
|
64
|
+
"better-sqlite3": "^12.8.0",
|
|
65
|
+
"commander": "^14.0.3",
|
|
66
|
+
"dotenv": "^17.4.0",
|
|
67
|
+
"pino": "^10.3.1",
|
|
68
|
+
"telegraf": "^4.16.3",
|
|
69
|
+
"tsx": "^4.21.0",
|
|
70
|
+
"zod": "^4.3.6"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
74
|
+
"@types/node": "^25.5.2",
|
|
75
|
+
"pino-pretty": "^13.1.3",
|
|
76
|
+
"tsup": "^8.5.1",
|
|
77
|
+
"typescript": "^6.0.2"
|
|
78
|
+
}
|
|
79
|
+
}
|
package/personality.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Co-Assistant Personality
|
|
2
|
+
|
|
3
|
+
> **Edit this file to change how the AI responds.**
|
|
4
|
+
> The contents are prepended to every message as system-level instructions.
|
|
5
|
+
> Changes take effect on the next message — no restart required.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Identity
|
|
10
|
+
|
|
11
|
+
You are **Co-Assistant**, a personal AI assistant that communicates through Telegram.
|
|
12
|
+
You are powered by the GitHub Copilot SDK and have access to tools (email, calendar, etc.)
|
|
13
|
+
provided by plugins. You act on behalf of the user — your job is to be genuinely useful.
|
|
14
|
+
|
|
15
|
+
## Tone & Style
|
|
16
|
+
|
|
17
|
+
- **Professional yet warm** — like a reliable colleague, not a corporate robot.
|
|
18
|
+
- **Concise by default** — get to the point. No filler phrases like "Sure!", "Of course!",
|
|
19
|
+
"Absolutely!", or "Great question!". Just answer.
|
|
20
|
+
- **Expand when it matters** — if the topic is complex or the user explicitly asks for detail,
|
|
21
|
+
give a thorough response. Use your judgment.
|
|
22
|
+
- **Plain language** — avoid jargon unless the user uses it first. No buzzwords.
|
|
23
|
+
- **Structured when helpful** — use bullet points, numbered lists, or short paragraphs for
|
|
24
|
+
clarity. Avoid walls of text.
|
|
25
|
+
|
|
26
|
+
## Thinking & Decision-Making
|
|
27
|
+
|
|
28
|
+
- **Be proactive** — if you notice something the user likely wants (e.g. a follow-up action,
|
|
29
|
+
a related piece of information), mention it briefly.
|
|
30
|
+
- **Ask before acting** — for irreversible actions (sending emails, deleting events, etc.),
|
|
31
|
+
confirm with the user first. Read-only actions (searching, listing) are fine to do immediately.
|
|
32
|
+
- **Admit uncertainty** — if you don't know something or a tool call fails, say so plainly.
|
|
33
|
+
Don't guess or fabricate information.
|
|
34
|
+
- **Use tools efficiently** — when you have tools available, use them rather than speculating.
|
|
35
|
+
Check the calendar instead of saying "I think you might have…".
|
|
36
|
+
|
|
37
|
+
## Formatting (Telegram)
|
|
38
|
+
|
|
39
|
+
- Telegram supports basic Markdown: **bold**, _italic_, `code`, ```code blocks```.
|
|
40
|
+
- Keep messages under ~2000 characters when possible. Split longer responses naturally.
|
|
41
|
+
- Use emoji sparingly and purposefully (✅ for confirmations, ⚠️ for warnings, 📧 for email
|
|
42
|
+
actions, 📅 for calendar). Don't overdo it.
|
|
43
|
+
|
|
44
|
+
## Boundaries
|
|
45
|
+
|
|
46
|
+
- You are a helpful assistant, not a therapist, lawyer, or doctor. Redirect appropriately.
|
|
47
|
+
- Never share the user's credentials, tokens, or personal data in responses.
|
|
48
|
+
- If a request is unclear, ask one focused clarifying question rather than guessing.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Gmail Plugin
|
|
2
|
+
|
|
3
|
+
Send, read, and search Gmail messages via the Gmail REST API. This plugin is a
|
|
4
|
+
**reference implementation** showing how to build Co-Assistant plugins end-to-end.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
| Tool | Description |
|
|
9
|
+
| ---------------- | -------------------------------------------------------- |
|
|
10
|
+
| `search_emails` | Search Gmail using the same query syntax as the Gmail UI |
|
|
11
|
+
| `read_email` | Read the full content of an email by message ID |
|
|
12
|
+
| `send_email` | Compose and send a plain-text email |
|
|
13
|
+
|
|
14
|
+
## Getting Google OAuth2 Credentials
|
|
15
|
+
|
|
16
|
+
Follow these steps to obtain the credentials the plugin requires:
|
|
17
|
+
|
|
18
|
+
1. **Create a Google Cloud project**
|
|
19
|
+
- Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
|
20
|
+
- Create a new project (or select an existing one).
|
|
21
|
+
|
|
22
|
+
2. **Configure the OAuth consent screen**
|
|
23
|
+
- Navigate to **APIs & Services → OAuth consent screen**.
|
|
24
|
+
- Set to "External" for personal use, add your email as a test user.
|
|
25
|
+
|
|
26
|
+
3. **Enable the Gmail API**
|
|
27
|
+
- Navigate to **APIs & Services → Library**.
|
|
28
|
+
- Search for "Gmail API" and click **Enable**.
|
|
29
|
+
|
|
30
|
+
4. **Create OAuth2 credentials**
|
|
31
|
+
- Go to **APIs & Services → Credentials**.
|
|
32
|
+
- Click **Create Credentials → OAuth client ID**.
|
|
33
|
+
- Choose **Desktop app** as the application type (recommended — allows automatic localhost redirect).
|
|
34
|
+
- Download the **client secret JSON file**.
|
|
35
|
+
|
|
36
|
+
5. **Run the setup wizard**
|
|
37
|
+
```bash
|
|
38
|
+
npx tsx src/cli/index.ts setup --plugin gmail
|
|
39
|
+
```
|
|
40
|
+
The setup will:
|
|
41
|
+
- Ask for your downloaded JSON file path (extracts credentials, does not store the file)
|
|
42
|
+
- Open your browser to authorize Gmail access
|
|
43
|
+
- Capture the refresh token automatically via a local callback server
|
|
44
|
+
|
|
45
|
+
## Required Credentials
|
|
46
|
+
|
|
47
|
+
| Key | Description | Type |
|
|
48
|
+
| ---------------------- | ------------------------------ | ------- |
|
|
49
|
+
| `GMAIL_CLIENT_ID` | Google OAuth2 Client ID | `oauth` |
|
|
50
|
+
| `GMAIL_CLIENT_SECRET` | Google OAuth2 Client Secret | `oauth` |
|
|
51
|
+
| `GMAIL_REFRESH_TOKEN` | Google OAuth2 Refresh Token | `oauth` |
|
|
52
|
+
|
|
53
|
+
Configure them via the CLI:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
co-assistant plugin configure gmail
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or set them in `config.json` under `plugins.gmail.credentials`.
|
|
60
|
+
|
|
61
|
+
## Example Usage
|
|
62
|
+
|
|
63
|
+
Once configured, the AI assistant can use the tools naturally:
|
|
64
|
+
|
|
65
|
+
> **User:** Do I have any unread emails from Alice?
|
|
66
|
+
>
|
|
67
|
+
> The assistant calls `search_emails` with query `from:alice is:unread` and
|
|
68
|
+
> returns a summary of matching messages.
|
|
69
|
+
|
|
70
|
+
> **User:** Read message ID `18f1a2b3c4d5e6f7`
|
|
71
|
+
>
|
|
72
|
+
> The assistant calls `read_email` and returns the full subject, sender, date,
|
|
73
|
+
> and body text.
|
|
74
|
+
|
|
75
|
+
> **User:** Send an email to bob@example.com about tomorrow's meeting
|
|
76
|
+
>
|
|
77
|
+
> The assistant calls `send_email` with the recipient, a generated subject line,
|
|
78
|
+
> and the composed body text.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module gmail/auth
|
|
3
|
+
* @description Google OAuth2 helper for the Gmail plugin.
|
|
4
|
+
*
|
|
5
|
+
* Manages access-token acquisition using an offline refresh token.
|
|
6
|
+
* Uses the built-in `fetch` API (Node 18+) — no external HTTP library required.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Google's OAuth2 token endpoint used to exchange a refresh token for an access token. */
|
|
10
|
+
const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages Google OAuth2 tokens for Gmail API access.
|
|
14
|
+
*
|
|
15
|
+
* Holds the long-lived refresh token and transparently obtains short-lived
|
|
16
|
+
* access tokens, caching them until they expire.
|
|
17
|
+
*/
|
|
18
|
+
export class GmailAuth {
|
|
19
|
+
private readonly clientId: string;
|
|
20
|
+
private readonly clientSecret: string;
|
|
21
|
+
private readonly refreshToken: string;
|
|
22
|
+
|
|
23
|
+
/** Cached access token from the most recent refresh. */
|
|
24
|
+
private accessToken: string | null = null;
|
|
25
|
+
|
|
26
|
+
/** Epoch-millisecond timestamp at which {@link accessToken} expires. */
|
|
27
|
+
private tokenExpiresAt = 0;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param clientId - Google OAuth2 Client ID.
|
|
31
|
+
* @param clientSecret - Google OAuth2 Client Secret.
|
|
32
|
+
* @param refreshToken - Long-lived Google OAuth2 Refresh Token.
|
|
33
|
+
*/
|
|
34
|
+
constructor(clientId: string, clientSecret: string, refreshToken: string) {
|
|
35
|
+
this.clientId = clientId;
|
|
36
|
+
this.clientSecret = clientSecret;
|
|
37
|
+
this.refreshToken = refreshToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns `true` when all three required credentials are non-empty strings.
|
|
42
|
+
*/
|
|
43
|
+
isConfigured(): boolean {
|
|
44
|
+
return Boolean(this.clientId && this.clientSecret && this.refreshToken);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Obtain a valid access token, refreshing automatically when necessary.
|
|
49
|
+
*
|
|
50
|
+
* The token is cached in memory and reused until 60 seconds before its
|
|
51
|
+
* stated expiry to account for clock skew and network latency.
|
|
52
|
+
*
|
|
53
|
+
* @returns A valid Google OAuth2 access token.
|
|
54
|
+
* @throws {Error} If the token refresh request fails.
|
|
55
|
+
*/
|
|
56
|
+
async getAccessToken(): Promise<string> {
|
|
57
|
+
// Return the cached token if it is still valid (with a 60-second buffer).
|
|
58
|
+
if (this.accessToken && Date.now() < this.tokenExpiresAt - 60_000) {
|
|
59
|
+
return this.accessToken;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const body = new URLSearchParams({
|
|
63
|
+
client_id: this.clientId,
|
|
64
|
+
client_secret: this.clientSecret,
|
|
65
|
+
refresh_token: this.refreshToken,
|
|
66
|
+
grant_type: "refresh_token",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const response = await fetch(GOOGLE_TOKEN_ENDPOINT, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
72
|
+
body: body.toString(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const errorText = await response.text();
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Failed to refresh Google access token (${response.status}): ${errorText}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = (await response.json()) as {
|
|
83
|
+
access_token: string;
|
|
84
|
+
expires_in: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.accessToken = data.access_token;
|
|
88
|
+
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
|
89
|
+
|
|
90
|
+
return this.accessToken;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module gmail
|
|
3
|
+
* @description Gmail plugin entry point.
|
|
4
|
+
*
|
|
5
|
+
* Provides email searching, reading, and sending capabilities via the
|
|
6
|
+
* Gmail REST API. This plugin serves as a reference implementation for
|
|
7
|
+
* building Co-Assistant plugins.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
CoAssistantPlugin,
|
|
12
|
+
PluginContext,
|
|
13
|
+
ToolDefinition,
|
|
14
|
+
} from "../../src/plugins/types.js";
|
|
15
|
+
import { GmailAuth } from "./auth.js";
|
|
16
|
+
import { createGmailTools } from "./tools.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Factory function that creates a new Gmail plugin instance.
|
|
20
|
+
*
|
|
21
|
+
* The plugin follows the standard Co-Assistant lifecycle:
|
|
22
|
+
* 1. `initialize()` — sets up the OAuth2 auth helper and creates tool defs.
|
|
23
|
+
* 2. `getTools()` — returns the tool definitions for the AI session.
|
|
24
|
+
* 3. `destroy()` — cleans up resources (no-op for this plugin).
|
|
25
|
+
*
|
|
26
|
+
* @returns A fully-formed {@link CoAssistantPlugin} for Gmail integration.
|
|
27
|
+
*/
|
|
28
|
+
export default function createPlugin(): CoAssistantPlugin {
|
|
29
|
+
let auth: GmailAuth;
|
|
30
|
+
let toolDefs: ToolDefinition[];
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: "gmail",
|
|
34
|
+
name: "Gmail Plugin",
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
description: "Send, read, and search Gmail messages",
|
|
37
|
+
requiredCredentials: [
|
|
38
|
+
"GMAIL_CLIENT_ID",
|
|
39
|
+
"GMAIL_CLIENT_SECRET",
|
|
40
|
+
"GMAIL_REFRESH_TOKEN",
|
|
41
|
+
],
|
|
42
|
+
|
|
43
|
+
async initialize(context: PluginContext) {
|
|
44
|
+
auth = new GmailAuth(
|
|
45
|
+
context.credentials.GMAIL_CLIENT_ID,
|
|
46
|
+
context.credentials.GMAIL_CLIENT_SECRET,
|
|
47
|
+
context.credentials.GMAIL_REFRESH_TOKEN,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
toolDefs = createGmailTools(auth, context.logger);
|
|
51
|
+
context.logger.info("Gmail plugin initialized");
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
getTools(): ToolDefinition[] {
|
|
55
|
+
return toolDefs;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async destroy() {
|
|
59
|
+
// No persistent connections or resources to release.
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async healthCheck(): Promise<boolean> {
|
|
63
|
+
return auth.isConfigured();
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "gmail",
|
|
3
|
+
"name": "Gmail Plugin",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Send, read, and search Gmail messages via the Gmail API",
|
|
6
|
+
"author": "co-assistant",
|
|
7
|
+
"requiredCredentials": [
|
|
8
|
+
{ "key": "GMAIL_CLIENT_ID", "description": "Google OAuth2 Client ID", "type": "oauth" },
|
|
9
|
+
{ "key": "GMAIL_CLIENT_SECRET", "description": "Google OAuth2 Client Secret", "type": "oauth" },
|
|
10
|
+
{ "key": "GMAIL_REFRESH_TOKEN", "description": "Google OAuth2 Refresh Token", "type": "oauth" }
|
|
11
|
+
],
|
|
12
|
+
"dependencies": []
|
|
13
|
+
}
|