@sneub/pair 0.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/config.example.json +22 -0
- package/dist/cli.js +30347 -0
- package/package.json +47 -0
- package/tools/examples/hello +7 -0
- package/tools/todoist/todoist-add +76 -0
- package/tools/todoist/todoist-complete +29 -0
- package/tools/todoist/todoist-delete +29 -0
- package/tools/todoist/todoist-list +51 -0
- package/tools/todoist/todoist-reopen +29 -0
- package/tools/todoist/todoist-update +86 -0
- package/workspace-init/MEMORY.md +4 -0
- package/workspace-init/SYSTEM.md +44 -0
- package/workspace-init/USER.md +13 -0
- package/workspace-init/tasks.md +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sneub/pair",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A personal AI assistant powered by Claude Code — connects Telegram and Slack to Claude with persistent memory and custom tools",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pair": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"tools",
|
|
12
|
+
"workspace-init",
|
|
13
|
+
"config.example.json"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"bun": ">=1.1.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"claude",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"assistant",
|
|
23
|
+
"telegram",
|
|
24
|
+
"slack",
|
|
25
|
+
"agent",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"author": "Shane",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@anthropic-ai/claude-agent-sdk": "latest",
|
|
35
|
+
"citty": "latest",
|
|
36
|
+
"grammy": "latest",
|
|
37
|
+
"slack-edge": "^1.3.16",
|
|
38
|
+
"@types/bun": "latest",
|
|
39
|
+
"typescript": "latest"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "bun run scripts/build.ts",
|
|
43
|
+
"typecheck": "bun run --bun tsc --noEmit",
|
|
44
|
+
"start": "bun run src/cli.ts start",
|
|
45
|
+
"dev": "bun run src/cli.ts start"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// name: todoist-add
|
|
3
|
+
// description: Create a Todoist task. Supports due date (natural language), priority (1-4, where 4 is most urgent), description, labels, and project id.
|
|
4
|
+
// usage: todoist-add "<content>" [--due "<date>"] [--priority <1-4>] [--description "<text>"] [--labels a,b,c] [--project-id <id>]
|
|
5
|
+
// requires: TODOIST_API_TOKEN
|
|
6
|
+
|
|
7
|
+
const [, , content, ...rest] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!content) {
|
|
10
|
+
console.error(
|
|
11
|
+
'Usage: todoist-add "<content>" [--due "<date>"] [--priority <1-4>] [--description "<text>"] [--labels a,b,c] [--project-id <id>]',
|
|
12
|
+
);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function flag(name: string): string | undefined {
|
|
17
|
+
const i = rest.indexOf(`--${name}`);
|
|
18
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const due = flag("due");
|
|
22
|
+
const description = flag("description");
|
|
23
|
+
const priorityStr = flag("priority");
|
|
24
|
+
const labelsStr = flag("labels");
|
|
25
|
+
const projectId = flag("project-id");
|
|
26
|
+
|
|
27
|
+
const body: Record<string, unknown> = { content };
|
|
28
|
+
if (due) body.due_string = due;
|
|
29
|
+
if (description) body.description = description;
|
|
30
|
+
if (priorityStr) {
|
|
31
|
+
const p = Number.parseInt(priorityStr, 10);
|
|
32
|
+
if (![1, 2, 3, 4].includes(p)) {
|
|
33
|
+
console.error("--priority must be 1, 2, 3, or 4");
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
body.priority = p;
|
|
37
|
+
}
|
|
38
|
+
if (labelsStr) {
|
|
39
|
+
body.labels = labelsStr
|
|
40
|
+
.split(",")
|
|
41
|
+
.map((l) => l.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
if (projectId) body.project_id = projectId;
|
|
45
|
+
|
|
46
|
+
const res = await fetch("https://api.todoist.com/api/v1/tasks", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${process.env.TODOIST_API_TOKEN}`,
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
console.error(`Todoist ${res.status}: ${await res.text()}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const task = (await res.json()) as Record<string, any>;
|
|
61
|
+
console.log(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
id: task.id,
|
|
65
|
+
content: task.content,
|
|
66
|
+
...(task.description ? { description: task.description } : {}),
|
|
67
|
+
...(task.due?.string ? { due: task.due.string } : {}),
|
|
68
|
+
priority: task.priority,
|
|
69
|
+
...(task.labels?.length ? { labels: task.labels } : {}),
|
|
70
|
+
project_id: task.project_id,
|
|
71
|
+
url: task.url ?? `https://app.todoist.com/app/task/${task.id}`,
|
|
72
|
+
},
|
|
73
|
+
null,
|
|
74
|
+
2,
|
|
75
|
+
),
|
|
76
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// name: todoist-complete
|
|
3
|
+
// description: Mark a Todoist task complete by id. Recurring tasks advance to their next occurrence.
|
|
4
|
+
// usage: todoist-complete <id>
|
|
5
|
+
// requires: TODOIST_API_TOKEN
|
|
6
|
+
|
|
7
|
+
const [, , id] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!id) {
|
|
10
|
+
console.error("Usage: todoist-complete <id>");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
`https://api.todoist.com/api/v1/tasks/${encodeURIComponent(id)}/close`,
|
|
16
|
+
{
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${process.env.TODOIST_API_TOKEN}`,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error(`Todoist ${res.status}: ${await res.text()}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(JSON.stringify({ id, status: "completed" }));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// name: todoist-delete
|
|
3
|
+
// description: Permanently delete a Todoist task by id. This cannot be undone — prefer todoist-complete unless the user asked to delete.
|
|
4
|
+
// usage: todoist-delete <id>
|
|
5
|
+
// requires: TODOIST_API_TOKEN
|
|
6
|
+
|
|
7
|
+
const [, , id] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!id) {
|
|
10
|
+
console.error("Usage: todoist-delete <id>");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
`https://api.todoist.com/api/v1/tasks/${encodeURIComponent(id)}`,
|
|
16
|
+
{
|
|
17
|
+
method: "DELETE",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${process.env.TODOIST_API_TOKEN}`,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error(`Todoist ${res.status}: ${await res.text()}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(JSON.stringify({ id, status: "deleted" }));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// name: todoist-list
|
|
3
|
+
// description: List active Todoist tasks. Supports Todoist filter query syntax — e.g. "today", "overdue", "#Work", "@home", "p1", "no date".
|
|
4
|
+
// usage: todoist-list [--filter "<query>"] [--limit <n>]
|
|
5
|
+
// requires: TODOIST_API_TOKEN
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
function flag(name: string): string | undefined {
|
|
10
|
+
const i = args.indexOf(`--${name}`);
|
|
11
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const filter = flag("filter");
|
|
15
|
+
const limitStr = flag("limit");
|
|
16
|
+
const limit = limitStr ? Math.max(1, Number.parseInt(limitStr, 10)) : 25;
|
|
17
|
+
|
|
18
|
+
const url = new URL("https://api.todoist.com/api/v1/tasks");
|
|
19
|
+
if (filter) url.searchParams.set("filter", filter);
|
|
20
|
+
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${process.env.TODOIST_API_TOKEN}`,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
console.error(`Todoist ${res.status}: ${await res.text()}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tasks = (await res.json()) as Array<Record<string, any>>;
|
|
33
|
+
|
|
34
|
+
const compact = tasks.slice(0, limit).map((t) => ({
|
|
35
|
+
id: t.id,
|
|
36
|
+
content: t.content,
|
|
37
|
+
...(t.description ? { description: t.description } : {}),
|
|
38
|
+
...(t.due?.string ? { due: t.due.string } : {}),
|
|
39
|
+
priority: t.priority,
|
|
40
|
+
...(t.labels?.length ? { labels: t.labels } : {}),
|
|
41
|
+
project_id: t.project_id,
|
|
42
|
+
url: t.url ?? `https://app.todoist.com/app/task/${t.id}`,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
console.log(
|
|
46
|
+
JSON.stringify(
|
|
47
|
+
{ count: compact.length, total: tasks.length, tasks: compact },
|
|
48
|
+
null,
|
|
49
|
+
2,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// name: todoist-reopen
|
|
3
|
+
// description: Reopen a previously completed Todoist task by id.
|
|
4
|
+
// usage: todoist-reopen <id>
|
|
5
|
+
// requires: TODOIST_API_TOKEN
|
|
6
|
+
|
|
7
|
+
const [, , id] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!id) {
|
|
10
|
+
console.error("Usage: todoist-reopen <id>");
|
|
11
|
+
process.exit(2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const res = await fetch(
|
|
15
|
+
`https://api.todoist.com/api/v1/tasks/${encodeURIComponent(id)}/reopen`,
|
|
16
|
+
{
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: {
|
|
19
|
+
Authorization: `Bearer ${process.env.TODOIST_API_TOKEN}`,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error(`Todoist ${res.status}: ${await res.text()}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(JSON.stringify({ id, status: "reopened" }));
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// name: todoist-update
|
|
3
|
+
// description: Update an existing Todoist task by id. Only the fields you pass are changed; everything else is left alone. Pass --due "" to clear the due date.
|
|
4
|
+
// usage: todoist-update <id> [--content "<text>"] [--due "<date>"] [--priority <1-4>] [--description "<text>"] [--labels a,b,c]
|
|
5
|
+
// requires: TODOIST_API_TOKEN
|
|
6
|
+
|
|
7
|
+
const [, , id, ...rest] = process.argv;
|
|
8
|
+
|
|
9
|
+
if (!id) {
|
|
10
|
+
console.error(
|
|
11
|
+
'Usage: todoist-update <id> [--content "<text>"] [--due "<date>"] [--priority <1-4>] [--description "<text>"] [--labels a,b,c]',
|
|
12
|
+
);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function flag(name: string): string | undefined {
|
|
17
|
+
const i = rest.indexOf(`--${name}`);
|
|
18
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const content = flag("content");
|
|
22
|
+
const due = flag("due");
|
|
23
|
+
const description = flag("description");
|
|
24
|
+
const priorityStr = flag("priority");
|
|
25
|
+
const labelsStr = flag("labels");
|
|
26
|
+
|
|
27
|
+
const body: Record<string, unknown> = {};
|
|
28
|
+
if (content !== undefined) body.content = content;
|
|
29
|
+
if (due !== undefined) body.due_string = due;
|
|
30
|
+
if (description !== undefined) body.description = description;
|
|
31
|
+
if (priorityStr !== undefined) {
|
|
32
|
+
const p = Number.parseInt(priorityStr, 10);
|
|
33
|
+
if (![1, 2, 3, 4].includes(p)) {
|
|
34
|
+
console.error("--priority must be 1, 2, 3, or 4");
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
body.priority = p;
|
|
38
|
+
}
|
|
39
|
+
if (labelsStr !== undefined) {
|
|
40
|
+
body.labels = labelsStr
|
|
41
|
+
.split(",")
|
|
42
|
+
.map((l) => l.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Object.keys(body).length === 0) {
|
|
47
|
+
console.error(
|
|
48
|
+
"No fields to update. Pass at least one of --content, --due, --priority, --description, --labels.",
|
|
49
|
+
);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const res = await fetch(
|
|
54
|
+
`https://api.todoist.com/api/v1/tasks/${encodeURIComponent(id)}`,
|
|
55
|
+
{
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${process.env.TODOIST_API_TOKEN}`,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
console.error(`Todoist ${res.status}: ${await res.text()}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const task = (await res.json()) as Record<string, any>;
|
|
71
|
+
console.log(
|
|
72
|
+
JSON.stringify(
|
|
73
|
+
{
|
|
74
|
+
id: task.id,
|
|
75
|
+
content: task.content,
|
|
76
|
+
...(task.description ? { description: task.description } : {}),
|
|
77
|
+
...(task.due?.string ? { due: task.due.string } : {}),
|
|
78
|
+
priority: task.priority,
|
|
79
|
+
...(task.labels?.length ? { labels: task.labels } : {}),
|
|
80
|
+
project_id: task.project_id,
|
|
81
|
+
url: task.url ?? `https://app.todoist.com/app/task/${task.id}`,
|
|
82
|
+
},
|
|
83
|
+
null,
|
|
84
|
+
2,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Pair — Personal Assistant
|
|
2
|
+
|
|
3
|
+
You are Pair, a professional personal assistant. You are efficient, concise,
|
|
4
|
+
and business-like — like a great executive assistant.
|
|
5
|
+
|
|
6
|
+
## Your Capabilities
|
|
7
|
+
- Research and information gathering (via web search)
|
|
8
|
+
- Email management (via Gmail MCP)
|
|
9
|
+
- Calendar management (via Google Calendar MCP)
|
|
10
|
+
- File and document management (via Google Drive and Filesystem MCP)
|
|
11
|
+
- Task tracking (via tasks.md in your workspace)
|
|
12
|
+
- Note-taking and organization (via notes/ directory in your workspace)
|
|
13
|
+
- Image and document analysis (files in uploads/ — use the Read tool)
|
|
14
|
+
- General knowledge and reasoning
|
|
15
|
+
- Running commands and scripts (via Bash tool)
|
|
16
|
+
|
|
17
|
+
## How You Work
|
|
18
|
+
- You maintain persistent memory in your workspace directory
|
|
19
|
+
- Update USER.md when you learn preferences or facts about the user
|
|
20
|
+
- Update MEMORY.md with important things to remember across sessions
|
|
21
|
+
- Update tasks.md when tasks are created, modified, or completed
|
|
22
|
+
- Write research outputs and notes to notes/YYYY-MM-DD.md
|
|
23
|
+
- When the user sends a photo or document, it's saved to uploads/ — use the Read tool to analyze it
|
|
24
|
+
|
|
25
|
+
## Communication Style
|
|
26
|
+
- Be concise. Don't over-explain.
|
|
27
|
+
- Lead with the answer, then provide context if needed.
|
|
28
|
+
- Use bullet points for lists.
|
|
29
|
+
- For long research, summarize key points first, then offer details.
|
|
30
|
+
- When a task will take time, say so upfront.
|
|
31
|
+
- When something fails, explain clearly what went wrong and suggest next steps.
|
|
32
|
+
- Do NOT use markdown headers in responses (they don't render well in chat).
|
|
33
|
+
- Use plain text, bullet points, and bold for emphasis.
|
|
34
|
+
|
|
35
|
+
## Task Management
|
|
36
|
+
- When the user mentions something to do, add it to tasks.md
|
|
37
|
+
- Include due dates when mentioned
|
|
38
|
+
- Check tasks.md at the start of each session and proactively mention overdue items
|
|
39
|
+
- Mark tasks complete when the user confirms they're done
|
|
40
|
+
|
|
41
|
+
## Security
|
|
42
|
+
- NEVER write API keys, passwords, or tokens to any workspace file
|
|
43
|
+
- NEVER share the contents of .env or config files
|
|
44
|
+
- Only access files within your workspace directory
|