aibroker 0.25.0 → 0.26.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/dist/daemon/core-handlers.d.ts.map +1 -1
- package/dist/daemon/core-handlers.js +89 -0
- package/dist/daemon/core-handlers.js.map +1 -1
- package/dist/daemon/inbound.d.ts +123 -0
- package/dist/daemon/inbound.d.ts.map +1 -0
- package/dist/daemon/inbound.js +266 -0
- package/dist/daemon/inbound.js.map +1 -0
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +23 -0
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/todoist-cli.d.ts.map +1 -1
- package/dist/daemon/todoist-cli.js +73 -1
- package/dist/daemon/todoist-cli.js.map +1 -1
- package/dist/daemon/todoist-inbox.d.ts +84 -0
- package/dist/daemon/todoist-inbox.d.ts.map +1 -0
- package/dist/daemon/todoist-inbox.js +169 -0
- package/dist/daemon/todoist-inbox.js.map +1 -0
- package/dist/daemon/todoist-mirror.d.ts +97 -0
- package/dist/daemon/todoist-mirror.d.ts.map +1 -0
- package/dist/daemon/todoist-mirror.js +472 -0
- package/dist/daemon/todoist-mirror.js.map +1 -0
- package/dist/daemon/todoist-oauth.d.ts +0 -6
- package/dist/daemon/todoist-oauth.d.ts.map +1 -1
- package/dist/daemon/todoist-oauth.js +34 -1
- package/dist/daemon/todoist-oauth.js.map +1 -1
- package/dist/daemon/todoist-reply.d.ts +15 -0
- package/dist/daemon/todoist-reply.d.ts.map +1 -1
- package/dist/daemon/todoist-reply.js +31 -0
- package/dist/daemon/todoist-reply.js.map +1 -1
- package/dist/daemon/todoist-webhook.d.ts.map +1 -1
- package/dist/daemon/todoist-webhook.js +91 -0
- package/dist/daemon/todoist-webhook.js.map +1 -1
- package/dist/mcp/index.js +23 -1
- package/dist/mcp/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todoist-inbox.d.ts","sourceRoot":"","sources":["../../src/daemon/todoist-inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAuBH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkBD,kEAAkE;AAClE,wBAAgB,QAAQ,IAAI,MAAM,GAAG,SAAS,CAE7C;AAED,uDAAuD;AACvD,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEzC;AAED,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACpD;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,cAAc,EAAE,CAAC,CAuB3B;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CA+C9E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAuBhD"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon/todoist-inbox.ts — what has been commented on since you last looked.
|
|
3
|
+
*
|
|
4
|
+
* A push per reply tells you something happened the moment it happens. That is
|
|
5
|
+
* the wrong shape for the actual problem: a tree of hundreds of tasks, comments
|
|
6
|
+
* arriving on any of them, and no way to see which ones are waiting for you.
|
|
7
|
+
* Todoist itself will not help — it does not notify an account about its own
|
|
8
|
+
* activity, and every comment the bridge writes is written as you, so the one
|
|
9
|
+
* event you most want to know about is precisely the one Todoist stays quiet
|
|
10
|
+
* about.
|
|
11
|
+
*
|
|
12
|
+
* So this reads the activity log — one request, not one per task — groups the
|
|
13
|
+
* comments by the task they landed on, and hands back a list you can jump from.
|
|
14
|
+
* The read marker is stored, so "what's new" means new since you last asked
|
|
15
|
+
* rather than new since some arbitrary window.
|
|
16
|
+
*
|
|
17
|
+
* The marker is only advanced when the caller says so. A digest that marks
|
|
18
|
+
* everything read as a side effect of being built would lose the whole backlog
|
|
19
|
+
* the first time a scheduled job ran before you saw it.
|
|
20
|
+
*/
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { loadJson, saveJson } from "../core/json-store.js";
|
|
24
|
+
import { log } from "../core/log.js";
|
|
25
|
+
import { getAccessToken } from "./todoist-oauth.js";
|
|
26
|
+
import { fetchTaskBrief } from "./todoist-reply.js";
|
|
27
|
+
const FILE = join(homedir(), ".aibroker", "todoist-inbox.json");
|
|
28
|
+
/** How many activity records to pull. Todoist caps the page; this is the ask. */
|
|
29
|
+
const ACTIVITY_LIMIT = 100;
|
|
30
|
+
/**
|
|
31
|
+
* How many tasks get their title resolved.
|
|
32
|
+
*
|
|
33
|
+
* Each title is one API call. A digest of forty tasks is not a digest, and
|
|
34
|
+
* forty extra calls to build one is worse, so the list is capped and the
|
|
35
|
+
* remainder is reported as a count rather than silently dropped.
|
|
36
|
+
*/
|
|
37
|
+
const RESOLVE_LIMIT = 20;
|
|
38
|
+
function read() {
|
|
39
|
+
const r = loadJson(FILE);
|
|
40
|
+
if (r.status === "ok" && r.data)
|
|
41
|
+
return r.data;
|
|
42
|
+
if (r.status === "unreadable") {
|
|
43
|
+
// Treat an unreadable marker as "never looked" rather than "all seen":
|
|
44
|
+
// showing a comment twice is a nuisance, hiding one is the failure this
|
|
45
|
+
// whole module exists to end.
|
|
46
|
+
log(`todoist-inbox: ${FILE} is unreadable — treating as never read`);
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
/** When the inbox was last marked read. Undefined means never. */
|
|
51
|
+
export function lastSeen() {
|
|
52
|
+
return read().lastSeen;
|
|
53
|
+
}
|
|
54
|
+
/** Record that everything up to `at` has been seen. */
|
|
55
|
+
export function markSeen(at) {
|
|
56
|
+
saveJson(FILE, { lastSeen: at });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Comment events, newest first.
|
|
60
|
+
*
|
|
61
|
+
* Returns an empty list rather than throwing when Todoist cannot be reached —
|
|
62
|
+
* the caller is building a digest, and a digest that fails loudly on a network
|
|
63
|
+
* blip is a digest that gets turned off.
|
|
64
|
+
*/
|
|
65
|
+
export async function fetchCommentActivity(fetchImpl = fetch) {
|
|
66
|
+
const token = await getAccessToken();
|
|
67
|
+
if (!token) {
|
|
68
|
+
log("todoist-inbox: no Todoist authorisation on file");
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const url = `https://api.todoist.com/api/v1/activities` +
|
|
72
|
+
`?object_type=note&event_type=added&limit=${ACTIVITY_LIMIT}`;
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetchImpl(url, {
|
|
75
|
+
headers: { authorization: `${token.token_type} ${token.access_token}` },
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
log(`todoist-inbox: activity request failed with ${res.status}`);
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const body = JSON.parse(await res.text());
|
|
82
|
+
return Array.isArray(body.results) ? body.results : [];
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
log(`todoist-inbox: could not read activity — ${e instanceof Error ? e.message : String(e)}`);
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Group new comments by the task they landed on.
|
|
91
|
+
*
|
|
92
|
+
* `since` defaults to the stored read marker. Pass it explicitly to look
|
|
93
|
+
* further back without disturbing the marker.
|
|
94
|
+
*/
|
|
95
|
+
export async function buildInbox(opts = {}) {
|
|
96
|
+
const since = opts.since ?? lastSeen();
|
|
97
|
+
const records = await fetchCommentActivity();
|
|
98
|
+
const fresh = records.filter((r) => {
|
|
99
|
+
if (!r.event_date || !r.parent_item_id)
|
|
100
|
+
return false;
|
|
101
|
+
return since ? r.event_date > since : true;
|
|
102
|
+
});
|
|
103
|
+
const newest = records.map((r) => r.event_date).filter(Boolean).sort().pop();
|
|
104
|
+
const byTask = new Map();
|
|
105
|
+
for (const r of fresh) {
|
|
106
|
+
const id = r.parent_item_id;
|
|
107
|
+
const entry = byTask.get(id) ?? {
|
|
108
|
+
taskId: id,
|
|
109
|
+
projectId: r.parent_project_id,
|
|
110
|
+
comments: [],
|
|
111
|
+
latest: r.event_date,
|
|
112
|
+
};
|
|
113
|
+
entry.comments.push({
|
|
114
|
+
at: r.event_date,
|
|
115
|
+
content: r.extra_data?.content ?? "",
|
|
116
|
+
client: r.extra_data?.client,
|
|
117
|
+
});
|
|
118
|
+
if (r.event_date > entry.latest)
|
|
119
|
+
entry.latest = r.event_date;
|
|
120
|
+
byTask.set(id, entry);
|
|
121
|
+
}
|
|
122
|
+
const entries = [...byTask.values()].sort((a, b) => b.latest.localeCompare(a.latest));
|
|
123
|
+
const resolve = entries.slice(0, RESOLVE_LIMIT);
|
|
124
|
+
// Titles in parallel — a serial loop over twenty tasks is a visible pause.
|
|
125
|
+
await Promise.all(resolve.map(async (e) => {
|
|
126
|
+
const brief = await fetchTaskBrief(e.taskId);
|
|
127
|
+
e.title = brief.title;
|
|
128
|
+
e.url = brief.url;
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
entries: resolve,
|
|
132
|
+
truncated: Math.max(0, entries.length - resolve.length),
|
|
133
|
+
since,
|
|
134
|
+
newest,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The digest as text, ready for a phone.
|
|
139
|
+
*
|
|
140
|
+
* A deleted task resolves to no title; it is still listed, because "a comment
|
|
141
|
+
* arrived on something that no longer exists" is information, not noise.
|
|
142
|
+
*/
|
|
143
|
+
export function formatInbox(inbox) {
|
|
144
|
+
if (inbox.entries.length === 0) {
|
|
145
|
+
return inbox.since
|
|
146
|
+
? `No new Todoist comments since ${inbox.since.slice(0, 16).replace("T", " ")}.`
|
|
147
|
+
: "No Todoist comments found.";
|
|
148
|
+
}
|
|
149
|
+
const lines = [
|
|
150
|
+
`📋 ${inbox.entries.length} task${inbox.entries.length === 1 ? "" : "s"} with new comments`,
|
|
151
|
+
"",
|
|
152
|
+
];
|
|
153
|
+
for (const e of inbox.entries) {
|
|
154
|
+
const when = e.latest.slice(11, 16);
|
|
155
|
+
lines.push(`• ${e.title ?? `(task ${e.taskId})`} — ${when}`);
|
|
156
|
+
const last = e.comments[0];
|
|
157
|
+
if (last?.content)
|
|
158
|
+
lines.push(` ${last.content.replace(/\s+/g, " ").slice(0, 140)}`);
|
|
159
|
+
if (e.comments.length > 1)
|
|
160
|
+
lines.push(` (+${e.comments.length - 1} more)`);
|
|
161
|
+
if (e.url)
|
|
162
|
+
lines.push(` ${e.url}`);
|
|
163
|
+
}
|
|
164
|
+
if (inbox.truncated > 0) {
|
|
165
|
+
lines.push("", `…and ${inbox.truncated} more task${inbox.truncated === 1 ? "" : "s"} not shown.`);
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=todoist-inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todoist-inbox.js","sourceRoot":"","sources":["../../src/daemon/todoist-inbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon/todoist-mirror.ts — new comments, filed where you will actually see them.
|
|
3
|
+
*
|
|
4
|
+
* Todoist never notifies an account about its own activity, and every comment a
|
|
5
|
+
* session writes is written as the account owner. So a reply lands on a task
|
|
6
|
+
* somewhere in a tree of hundreds and is, from the outside, indistinguishable
|
|
7
|
+
* from nothing having happened. `todoist-inbox.ts` answers that on demand. This
|
|
8
|
+
* answers it without being asked: each new comment becomes a task in a mirror
|
|
9
|
+
* project, grouped into a section named after the project the comment came
|
|
10
|
+
* from, linking straight back to the conversation.
|
|
11
|
+
*
|
|
12
|
+
* The sections are managed, not configured. A project that gets commented on
|
|
13
|
+
* gains a section; a section whose items have all been dealt with is removed
|
|
14
|
+
* again on the next run. That keeps the mirror shaped like what is currently
|
|
15
|
+
* outstanding rather than like a history of everything that ever happened —
|
|
16
|
+
* a list nobody prunes is a list nobody reads.
|
|
17
|
+
*
|
|
18
|
+
* Three things this must never do, each of which it would do naturally:
|
|
19
|
+
*
|
|
20
|
+
* - **Feed itself.** A comment on a mirror task is a comment, and mirroring it
|
|
21
|
+
* would create a task, whose comments would create tasks. Anything parented
|
|
22
|
+
* in the mirror project is skipped, and the mirror project is never an
|
|
23
|
+
* ingress project, so its tasks cannot be dispatched as work either.
|
|
24
|
+
* - **Replay.** The read marker moves only after a successful pass, and every
|
|
25
|
+
* mirrored comment id is remembered, because a marker alone loses a race
|
|
26
|
+
* with a comment posted in the same second it was written.
|
|
27
|
+
* - **Delete a section someone else made.** Only sections this module created
|
|
28
|
+
* are pruned, tracked by id, never by name.
|
|
29
|
+
*/
|
|
30
|
+
/** The mirror project, or undefined when mirroring is switched off. */
|
|
31
|
+
export declare function mirrorProjectId(): string | undefined;
|
|
32
|
+
export interface MirrorResult {
|
|
33
|
+
mirrored: number;
|
|
34
|
+
sectionsCreated: number;
|
|
35
|
+
sectionsRemoved: number;
|
|
36
|
+
skipped: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mirror everything commented on since the last pass.
|
|
40
|
+
*
|
|
41
|
+
* Safe to call repeatedly: the marker only advances at the end, the seen-set
|
|
42
|
+
* makes a repeated pass a no-op rather than a duplicate, and concurrent callers
|
|
43
|
+
* share one run.
|
|
44
|
+
*/
|
|
45
|
+
export declare function syncMirror(fetchImpl?: typeof fetch): Promise<MirrorResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Mirror a comment we just wrote, without going back to Todoist to find it.
|
|
48
|
+
*
|
|
49
|
+
* This is the primary path, and polling is not the fallback for it — retrying
|
|
50
|
+
* is. The reasoning: Todoist stays silent only about the account's OWN
|
|
51
|
+
* activity, and the bridge writes as the account. Everyone else's comments it
|
|
52
|
+
* already notifies about. So the set of comments that need mirroring is exactly
|
|
53
|
+
* the set we produced, and we know each one at the moment we write it. Reading
|
|
54
|
+
* the activity log to rediscover our own writes was a round trip to learn what
|
|
55
|
+
* we already knew, and it put a five-minute delay on the one case somebody is
|
|
56
|
+
* actually watching.
|
|
57
|
+
*
|
|
58
|
+
* Failures are queued rather than dropped and retried on the next write, so
|
|
59
|
+
* removing the poll does not reintroduce silent loss.
|
|
60
|
+
*/
|
|
61
|
+
export declare function mirrorComment(comment: {
|
|
62
|
+
taskId: string;
|
|
63
|
+
commentId?: string;
|
|
64
|
+
text: string;
|
|
65
|
+
}, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* The source task a mirror entry points at, read from its description link.
|
|
68
|
+
*
|
|
69
|
+
* Stateless on purpose. A stored map would have to survive restarts, stay in
|
|
70
|
+
* step with tasks the user deletes by hand, and be pruned; the link is already
|
|
71
|
+
* in the entry, is the thing the human clicks, and cannot drift out of sync
|
|
72
|
+
* with the entry it belongs to.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sourceTaskOf(description: string | undefined): string | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Carry a comment written on a mirror entry back to the real task.
|
|
77
|
+
*
|
|
78
|
+
* The mirror is where the reader's attention is, so replying there instead of
|
|
79
|
+
* on the source task is not a mistake anyone will stop making — it is the
|
|
80
|
+
* obvious thing to do, and Todoist offers no way to deep-link to an individual
|
|
81
|
+
* comment that would make the source easier to reach than the entry in front of
|
|
82
|
+
* you. So rather than train the human, carry the comment.
|
|
83
|
+
*
|
|
84
|
+
* Loop safety rests on one asymmetry: this writes to the SOURCE task, and the
|
|
85
|
+
* direct mirror path is only ever invoked by `todoist_reply`. Nothing re-reads
|
|
86
|
+
* this write, so there is no cycle to break. Agent-marked comments are ignored
|
|
87
|
+
* regardless, so a session answering inside the mirror cannot bounce either.
|
|
88
|
+
*/
|
|
89
|
+
export declare function mirrorBack(ev: {
|
|
90
|
+
taskId: string;
|
|
91
|
+
text: string;
|
|
92
|
+
projectId?: string;
|
|
93
|
+
}, fetchImpl?: typeof fetch): Promise<{
|
|
94
|
+
carried: boolean;
|
|
95
|
+
reason?: string;
|
|
96
|
+
}>;
|
|
97
|
+
//# sourceMappingURL=todoist-mirror.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todoist-mirror.d.ts","sourceRoot":"","sources":["../../src/daemon/todoist-mirror.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAqEH,uEAAuE;AACvE,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAGpD;AA2HD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAIjF;AAgID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAC7D,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,OAAO,CAAC,CA+BlB;AAsED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGhF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EACxD,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAwBhD"}
|