taskswipe-mcp 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # taskswipe-mcp
2
2
 
3
- Your TASK//SWIPE day, from inside Claude Code, Claude Desktop, Codex or Cursor.
3
+ Your TASKKA day, from inside Claude Code, Claude Desktop, Codex or Cursor.
4
4
 
5
5
  ```
6
6
  you: add "call the bank" and "book the dentist" to today
@@ -27,15 +27,14 @@ token over the wire.
27
27
 
28
28
  ## Setup
29
29
 
30
- You need your TASK//SWIPE login token — that's it. The production Supabase URL
30
+ You need your TASKKA login token — that's it. The production Supabase URL
31
31
  and anon key are baked in as defaults (they're public — they ship in the web
32
32
  app's JS bundle); self-hosters can override them with `TASKSWIPE_SUPABASE_URL`
33
33
  and `TASKSWIPE_SUPABASE_ANON_KEY`.
34
34
 
35
- > **Not on npm yet.** Until `taskswipe-mcp` is published, every client below
36
- > runs it from this repo by path:
37
- > `node ~/Developer/swipe-todo-react/mcp/src/index.js`
38
- > Once it's published, replace that with `npx -y taskswipe-mcp` everywhere.
35
+ > **Published:** https://www.npmjs.com/package/taskswipe-mcp — so anywhere the
36
+ > examples below run it by path, `npx -y taskswipe-mcp` works instead and needs
37
+ > nothing checked out.
39
38
 
40
39
  **Claude Code**
41
40
 
@@ -74,6 +73,50 @@ TASKSWIPE_ACCESS_TOKEN = "eyJ..."
74
73
  (`codex mcp list` to confirm it registered; note the section is
75
74
  `mcp_servers`, snake_case, not `mcpServers`.)
76
75
 
76
+ **Hermes Agent** (Nous) — local stdio, tokens refresh themselves. Paste into `~/.hermes/config.yaml`, then install the skill:
77
+
78
+ ```yaml
79
+ mcp_servers:
80
+ taskka:
81
+ command: "npx"
82
+ args: ["-y", "taskswipe-mcp"]
83
+ env:
84
+ TASKSWIPE_ACCESS_TOKEN: "eyJ..."
85
+ TASKSWIPE_REFRESH_TOKEN: "..."
86
+ ```
87
+
88
+ ```bash
89
+ hermes skills install https://taskka.app/.well-known/skills/taskka/SKILL.md
90
+ ```
91
+
92
+ Copy both snippets from TASKKA → SETTINGS → AGENTS (signed in) so the tokens are live.
93
+
94
+ **OpenClaw** — same local server. Paste into `openclaw.json` (or Control UI → Settings → MCP):
95
+
96
+ ```json
97
+ {
98
+ "mcp": {
99
+ "servers": {
100
+ "taskka": {
101
+ "command": "npx",
102
+ "args": ["-y", "taskswipe-mcp"],
103
+ "env": {
104
+ "TASKSWIPE_ACCESS_TOKEN": "eyJ...",
105
+ "TASKSWIPE_REFRESH_TOKEN": "..."
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ```bash
114
+ openclaw skills install https://taskka.app/.well-known/skills/taskka/SKILL.md
115
+ openclaw mcp doctor taskka --probe
116
+ ```
117
+
118
+ Hosted HTTP (`https://taskka.app/mcp` + `Authorization: Bearer <access>`) works for a Control-UI one-shot. Access tokens die in about an hour — always-on Hermes/OpenClaw gateways should use stdio above.
119
+
77
120
  **Claude Desktop / Cursor** — these do use the JSON shape
78
121
  (`claude_desktop_config.json` / `.cursor/mcp.json`):
79
122
 
@@ -95,7 +138,7 @@ TASKSWIPE_ACCESS_TOKEN = "eyJ..."
95
138
 
96
139
  ### Getting an access token
97
140
 
98
- Sign in to TASK//SWIPE in a browser, open the console, and run:
141
+ Sign in to TASKKA in a browser, open the console, and run:
99
142
 
100
143
  ```js
101
144
  JSON.parse(Object.entries(localStorage).find(([k]) => k.includes('auth-token'))[1]).access_token
@@ -129,9 +172,7 @@ The repo root carries `.codex-plugin/plugin.json` + `.mcp.json`, which makes
129
172
  the whole repo an installable Codex plugin. The path to being listed
130
173
  (as of 2026-08):
131
174
 
132
- 1. **Publish `taskswipe-mcp` to npm** — the plugin manifest runs
133
- `npx -y taskswipe-mcp`, which is dead until the package exists.
134
- `cd mcp && npm publish --access public` under an npm account.
175
+ 1. ~~Publish to npm~~ — **done**, `taskswipe-mcp@0.1.0`.
135
176
  2. **Interim listing** — submit the GitHub repo URL to the community registry
136
177
  at codex-marketplace.com (automated review of the manifest).
137
178
  3. **Official directory** — OpenAI's self-serve publishing is "coming soon";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "taskswipe-mcp",
3
- "version": "0.1.0",
4
- "description": "Read and write your TASK//SWIPE day from Claude Code, Claude Desktop, Codex or Cursor.",
3
+ "version": "0.2.1",
4
+ "description": "Read and write your TASKKA day from Claude Code, Claude Desktop, Codex or Cursor.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "taskswipe-mcp": "src/index.js"
package/src/brief.js ADDED
@@ -0,0 +1,384 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // THE BRIEF — the whole system, in about a page.
3
+ //
4
+ // THE PROBLEM. An agent connecting to TASKKA could reach six tools covering
5
+ // three of twenty-five tabs, and had no way to find out what the other
6
+ // twenty-two even were. Answering "how am I doing?" meant guessing which tools
7
+ // existed, calling several, and stitching. Answering anything about food, lifts,
8
+ // money, habits, goals or study was impossible — not because the data was
9
+ // private but because nothing described it.
10
+ //
11
+ // The lazy fix is get_state. That hands over a few hundred KB of arrays so the
12
+ // model can count them itself: slow, expensive, and mostly a waste, because
13
+ // ninety-nine percent of that blob is history nobody asked about.
14
+ //
15
+ // ── WHAT THIS DOES INSTEAD ──────────────────────────────────────────────────
16
+ //
17
+ // SUMMARISE, NEVER SERIALISE. No array goes out whole. Everything is a count,
18
+ // a headline, or the two or three items that actually need a decision. The
19
+ // brief is bounded — roughly the same size for someone on day 900 as on day 3 —
20
+ // because size here should track how much is GOING ON, not how much has ever
21
+ // happened.
22
+ //
23
+ // SIGNALS, NOT DATA. "3 habits missed 4 days" is the sentence an agent can act
24
+ // on. The habits array is a puzzle it has to solve first, badly, every time.
25
+ //
26
+ // SAY WHAT EXISTS. `areas` lists every part of the app with a one-line state,
27
+ // including the empty ones — an agent cannot ask about a tab it has no idea is
28
+ // there, and "nothing logged" is a useful answer to "what should I do next?".
29
+ //
30
+ // LAYERED. The brief is the map. `detail(area)` is the drill-down, and the
31
+ // existing tools do the writing. One cheap call to orient, one targeted call
32
+ // for depth — instead of six calls and a guess.
33
+ //
34
+ // Pure and state-in/JSON-out on purpose, so it can be tested without a network.
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+
37
+ const arr = (v) => (Array.isArray(v) ? v : [])
38
+ const num = (v) => (Number.isFinite(+v) ? +v : 0)
39
+
40
+ const dayStr = (d = new Date()) =>
41
+ `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
42
+
43
+ const daysAgo = (n, from = new Date()) => {
44
+ const d = new Date(from)
45
+ d.setDate(d.getDate() - n)
46
+ return dayStr(d)
47
+ }
48
+
49
+ const lastN = (n, from = new Date()) => Array.from({ length: n }, (_, i) => daysAgo(i, from))
50
+
51
+ /** A completion is 'YYYY-MM-DD' or {date, kind}; a MISSED day is not a doing. */
52
+ const stackDoneDays = (s) => new Set(arr(s?.completions)
53
+ .filter(c => (typeof c === 'string' ? true : c?.kind !== 'missed'))
54
+ .map(c => (typeof c === 'string' ? c : c?.date))
55
+ .filter(Boolean))
56
+
57
+ const habitDoneDays = (h) => new Set(arr(h?.completions).map(c => (typeof c === 'string' ? c : c?.date)).filter(Boolean))
58
+
59
+ /** How many of the last `win` days had a tick — the honest read of a streak. */
60
+ const hitRate = (days, win, from) => lastN(win, from).reduce((n, d) => n + (days.has(d) ? 1 : 0), 0)
61
+
62
+ const partOfDay = (h) => (h < 5 ? 'night' : h < 12 ? 'morning' : h < 17 ? 'afternoon' : h < 22 ? 'evening' : 'night')
63
+
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+
66
+ export function buildBrief(state = {}, now = new Date()) {
67
+ const today = dayStr(now)
68
+ const S = state || {}
69
+
70
+ // ── TODAY ─────────────────────────────────────────────────────────────────
71
+ const tasks = arr(S.todayTasks)
72
+ const open = tasks.filter(t => !t.done)
73
+ const done = tasks.filter(t => t.done)
74
+ const timed = open.filter(t => t.at).sort((a, b) => String(a.at).localeCompare(String(b.at)))
75
+ const hhmm = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
76
+
77
+ // ── ATTENTION: the things a person would want raised ──────────────────────
78
+ // Ordered by severity so an agent can take the top one and be right most of
79
+ // the time. This is the part worth reading if it reads nothing else.
80
+ const attention = []
81
+
82
+ const overdue = timed.filter(t => String(t.at) < hhmm)
83
+ if (overdue.length) {
84
+ attention.push({ area: 'today', severity: 3,
85
+ what: `${overdue.length} scheduled ${overdue.length === 1 ? 'task is' : 'tasks are'} past their time`,
86
+ items: overdue.slice(0, 3).map(t => `${t.at} ${t.text}`) })
87
+ }
88
+
89
+ const missed = arr(S.missedTasks)
90
+ if (missed.length) {
91
+ attention.push({ area: 'dropped', severity: missed.length >= 3 ? 2 : 1,
92
+ what: `${missed.length} unfinished from earlier days sitting in LOG → DROPPED`,
93
+ items: missed.slice(0, 4).map(t => t.text) })
94
+ }
95
+
96
+ // Habits are the app's committed tier: quiet ones are the real signal.
97
+ const quietHabits = arr(S.habits)
98
+ .map(h => ({ name: h.name || h.title, hits: hitRate(habitDoneDays(h), 7, now) }))
99
+ .filter(h => h.hits === 0)
100
+ if (quietHabits.length) {
101
+ attention.push({ area: 'habits', severity: 2,
102
+ what: `${quietHabits.length} habit${quietHabits.length === 1 ? '' : 's'} untouched for a week`,
103
+ items: quietHabits.slice(0, 4).map(h => h.name) })
104
+ }
105
+
106
+ const decay = S.decayState || {}
107
+ if (num(decay.quietStreak) >= 2) {
108
+ attention.push({ area: 'squares', severity: 2,
109
+ what: `${decay.quietStreak} quiet days in a row — squares are decaying` })
110
+ }
111
+
112
+ if (!S.coreTask && open.length > 3) {
113
+ attention.push({ area: 'today', severity: 1,
114
+ what: `${open.length} things open and nothing named as the one that matters` })
115
+ }
116
+
117
+ attention.sort((a, b) => b.severity - a.severity)
118
+
119
+ // ── AREAS: one line each, including the empty ones ────────────────────────
120
+ const foodToday = arr(S.foodLog?.[today])
121
+ const kcal = Math.round(foodToday.reduce((n, e) =>
122
+ n + (num(e.per100?.kcal) * num(e.grams)) / 100, 0))
123
+ const health = S.healthDaily?.[today] || {}
124
+
125
+ const stacks = arr(S.stacks).filter(s => !s.retiredAt).map(s => ({
126
+ name: s.name,
127
+ hits: hitRate(stackDoneDays(s), Math.round((num(s.graduationTarget) || 14) * 1.5), now),
128
+ target: num(s.graduationTarget) || 14,
129
+ }))
130
+ const readyStacks = stacks.filter(s => s.hits >= s.target)
131
+
132
+ const goalFrames = S.goals && typeof S.goals === 'object' ? Object.values(S.goals).flat() : []
133
+ const building = goalFrames.find(g => g?.id === S.activeGoalId)
134
+
135
+ const maps = arr(S.studyMaps)
136
+ const learningTopics = maps.flatMap(m => arr(m.nodes).filter(n => n.status === 'learning')
137
+ .map(n => `${m.title}: ${n.title}`))
138
+
139
+ const biz = arr(S.businessIdeas)
140
+ const areas = {
141
+ today: `${open.length} open, ${done.length} done${S.coreTask ? `, core: ${S.coreTask.task || S.coreTask}` : ''}`,
142
+ dropped: missed.length
143
+ ? `${missed.length} unfinished from earlier days (LOG → DROPPED)`
144
+ : 'nothing sitting in DROPPED',
145
+ backlog: `${arr(S.backlogTasks).length} parked`,
146
+ docket: `${arr(S.docketTasks).length} coming-up with no clock`,
147
+ deck: `${arr(S.deck).length} cards waiting to be swiped`,
148
+ recurring: `${arr(S.recurringTasks).length} rules`,
149
+ lists: `${arr(S.lists).length} lists`,
150
+ frame: `${arr(S.frameworks).length} living frameworks`,
151
+ business: biz.length
152
+ ? `${biz.filter(i => i?.stage !== 'ship' && i?.stage !== 'drop').length} live ideas on the spark-to-ship path`
153
+ : 'no ideas on the path',
154
+ people: `${arr(S.people).length} people`,
155
+ things: `${arr(S.things).length} kept`,
156
+ missions: `${arr(S.missions).filter(m => !m.done).length} open`,
157
+ goals: goalFrames.length
158
+ ? `${goalFrames.length} goals${building ? `, building: ${building.text}` : ', none on the crane'}`
159
+ : 'none set',
160
+ habits: arr(S.habits).length
161
+ ? `${arr(S.habits).length} habits, ${arr(S.habits).filter(h => habitDoneDays(h).has(today)).length} ticked today`
162
+ : 'none',
163
+ stacks: stacks.length
164
+ ? `${stacks.length} building${readyStacks.length ? `, ${readyStacks.length} ready to graduate` : ''}`
165
+ : 'none',
166
+ food: foodToday.length
167
+ ? `${kcal} kcal logged today, ${arr(S.pantry).length} foods on the shelf`
168
+ : `nothing logged today, ${arr(S.pantry).length} foods on the shelf`,
169
+ lift: `${arr(S.liftSessions).length} sessions logged`,
170
+ walk: health.steps ? `${health.steps} steps today` : 'no step data today',
171
+ money: arr(S.moneyTx).length ? `${arr(S.moneyTx).length} transactions` : 'nothing tracked',
172
+ mind: S.moods?.[today]?.length
173
+ ? `${S.moods[today].length} check-ins today`
174
+ : 'no check-in today',
175
+ day_score: S.dayScores?.[today] ? `${S.dayScores[today].score}/10 posted` : 'not posted',
176
+ study: maps.length
177
+ ? `${maps.length} maps, ${learningTopics.length} topics in progress`
178
+ : 'none',
179
+ reader: `${arr(S.books).length} books`,
180
+ parley: `${arr(S.parleyDeck).length} cards`,
181
+ squares: `${num(S.totalSquares)} total${num(decay.quietStreak) ? `, ${decay.quietStreak} quiet days` : ''}`,
182
+ }
183
+
184
+ return {
185
+ now: { date: today, time: hhmm, part_of_day: partOfDay(now.getHours()) },
186
+
187
+ // The one sentence an agent should lead with. Everything below is evidence.
188
+ headline: attention[0]?.what
189
+ || (open.length ? `${open.length} open today, nothing overdue` : 'nothing open today'),
190
+
191
+ today: {
192
+ core_task: S.coreTask?.task || S.coreTask || null,
193
+ open: open.length,
194
+ done: done.length,
195
+ // Only the ones with a time — an untimed list of forty is not a schedule.
196
+ next: timed.filter(t => String(t.at) >= hhmm).slice(0, 3).map(t => `${t.at} ${t.text}`),
197
+ overdue: overdue.slice(0, 5).map(t => `${t.at} ${t.text}`),
198
+ open_titles: open.slice(0, 12).map(t => t.text),
199
+ },
200
+
201
+ attention: attention.slice(0, 6),
202
+ areas,
203
+
204
+ // Where to look next, so an agent does not have to guess which tools exist.
205
+ drill: {
206
+ more_on_an_area: "detail(area) — any key from `areas`",
207
+ today_in_full: 'today()',
208
+ dropped_in_full: 'detail({ area: "dropped" })',
209
+ find_anything: 'search(query)',
210
+ write: 'add_tasks, complete_task, capture, set_core_task, recover_dropped',
211
+ },
212
+ }
213
+ }
214
+
215
+ // ─────────────────────────────────────────────────────────────────────────────
216
+ // DETAIL — one area, still summarised.
217
+ //
218
+ // The point of splitting this out is that an agent pays for depth only where it
219
+ // asked for it. Each branch returns the shape someone would actually reason
220
+ // with, not the rows.
221
+ // ─────────────────────────────────────────────────────────────────────────────
222
+
223
+ export function buildDetail(state = {}, area = '', now = new Date()) {
224
+ const S = state || {}
225
+ const today = dayStr(now)
226
+ const key = String(area || '').toLowerCase().trim()
227
+
228
+ switch (key) {
229
+ case 'today': {
230
+ const tasks = arr(S.todayTasks)
231
+ return {
232
+ open: tasks.filter(t => !t.done).map(t => ({
233
+ text: t.text, at: t.at || null, category: t.category || null,
234
+ why: t.why || null, timer: t.timer || null, sticky: !!t.sticky,
235
+ })),
236
+ done: tasks.filter(t => t.done).map(t => t.text),
237
+ dropped: arr(S.missedTasks).slice(0, 20).map(t => t.text),
238
+ }
239
+ }
240
+ case 'dropped':
241
+ case 'log':
242
+ return {
243
+ where: 'LOG → DROPPED. Tasks that aged out unfinished from earlier days. Not the backlog, not CUT.',
244
+ count: arr(S.missedTasks).length,
245
+ items: arr(S.missedTasks).slice(0, 40).map(t => ({
246
+ text: t.text,
247
+ expired: t.expiredDate || null,
248
+ category: t.category || null,
249
+ })),
250
+ recover: 'recover_dropped({ task }) puts one back on today',
251
+ }
252
+ case 'docket':
253
+ case 'calendar':
254
+ return {
255
+ docket: arr(S.docketTasks).slice(0, 40).map(t => t.text || t.task),
256
+ planned_days: Object.keys(S.plannedTasks || {}).length,
257
+ }
258
+ case 'lists':
259
+ return arr(S.lists).slice(0, 30).map(l => ({
260
+ name: l.title || l.name, items: arr(l.items).length,
261
+ }))
262
+ case 'frame':
263
+ return arr(S.frameworks).slice(0, 20).map(f => ({
264
+ name: f.name, when: f.when || null, decisions: arr(f.decisions).length,
265
+ }))
266
+ case 'business':
267
+ return arr(S.businessIdeas).slice(0, 40).map(i => ({
268
+ title: i.title, stage: i.stage || 'spark', next: i.next || null,
269
+ }))
270
+ case 'people':
271
+ return arr(S.people).slice(0, 40).map(p => ({
272
+ name: p.name, role: p.role || null, company: p.company || null,
273
+ }))
274
+ case 'things':
275
+ return { kept: arr(S.things).length, categories: arr(S.thingsCats).length }
276
+ case 'habits':
277
+ return arr(S.habits).map(h => {
278
+ const days = habitDoneDays(h)
279
+ return { name: h.name || h.title, today: days.has(today), last_7: hitRate(days, 7, now), lifetime: days.size }
280
+ })
281
+ case 'stacks':
282
+ return arr(S.stacks).map(s => {
283
+ const days = stackDoneDays(s)
284
+ const target = num(s.graduationTarget) || 14
285
+ const window = Math.round(target * 1.5)
286
+ return {
287
+ name: s.name, cue: s.anchor || null, minimum: s.minimum || null,
288
+ retired: !!s.retiredAt,
289
+ hits: hitRate(days, window, now), target, window,
290
+ ready: hitRate(days, window, now) >= target,
291
+ today: days.has(today),
292
+ }
293
+ })
294
+ case 'goals': {
295
+ const frames = S.goals && typeof S.goals === 'object' ? S.goals : {}
296
+ return Object.entries(frames).flatMap(([frame, list]) => arr(list).map(g => ({
297
+ frame, text: g.text, bricks: `${num(g.earnedBricks)}/${num(g.targetBricks) || 20}`,
298
+ building: g.id === S.activeGoalId,
299
+ steps: arr(g.steps).length ? arr(g.steps).map(st => `${st.stage}: ${st.text}`) : null,
300
+ })))
301
+ }
302
+ case 'food': {
303
+ const day = arr(S.foodLog?.[today])
304
+ const shopping = arr(S.foodShoppingList)
305
+ const openShopping = shopping.filter(row => !row.done)
306
+ const tot = day.reduce((a, e) => {
307
+ const f = num(e.grams) / 100
308
+ return {
309
+ kcal: a.kcal + num(e.per100?.kcal) * f,
310
+ protein: a.protein + num(e.per100?.protein) * f,
311
+ carbs: a.carbs + num(e.per100?.carbs) * f,
312
+ fat: a.fat + num(e.per100?.fat) * f,
313
+ }
314
+ }, { kcal: 0, protein: 0, carbs: 0, fat: 0 })
315
+ return {
316
+ today: day.map(e => `${e.meal}: ${e.name} ${e.grams}${e.unit || 'g'}`),
317
+ totals: Object.fromEntries(Object.entries(tot).map(([k, v]) => [k, Math.round(v)])),
318
+ targets: S.foodTargets || null,
319
+ pantry_size: arr(S.pantry).length,
320
+ pantry_catalog_size: arr(S.pantry).length,
321
+ pantry_stock: arr(S.pantryStock).slice(0, 100).map(row => ({
322
+ name: row.name,
323
+ quantity: num(row.quantity),
324
+ unit: row.unit || 'each',
325
+ location: row.location || null,
326
+ expires_on: row.expiresOn || null,
327
+ minimum: num(row.minimum),
328
+ })),
329
+ shopping_list: openShopping.slice(0, 60).map(row => ({
330
+ name: row.name,
331
+ quantity: num(row.quantity) || 1,
332
+ unit: row.unit || 'each',
333
+ source: row.source || 'manual',
334
+ })),
335
+ shopping_counts: {
336
+ open: openShopping.length,
337
+ bought: shopping.length - openShopping.length,
338
+ returned: Math.min(openShopping.length, 60),
339
+ },
340
+ ordering_rules: String(S.aiPlaybookOverlays?.food || '').slice(0, 4000) || null,
341
+ saved_meals: arr(S.meals).map(m => m.name),
342
+ }
343
+ }
344
+ case 'study':
345
+ return arr(S.studyMaps).map(m => ({
346
+ map: m.title, aim: m.blurb || null,
347
+ topics: arr(m.nodes).map(n => ({
348
+ title: n.title, status: n.status,
349
+ // The tutor's note is the memory of this topic — the single most
350
+ // useful thing an agent can read about someone's learning.
351
+ next: (n.note || '').split(/^##\s+Next\s*$/mi)[1]?.split('\n').map(l => l.trim()).find(Boolean) || null,
352
+ })),
353
+ }))
354
+ case 'lift':
355
+ return {
356
+ sessions: arr(S.liftSessions).slice(-8).map(s => ({
357
+ date: s.date, name: s.name || null,
358
+ exercises: arr(s.exercises).map(e => e.name).slice(0, 12),
359
+ })),
360
+ routines: arr(S.liftRoutines).map(r => r.name),
361
+ }
362
+ case 'mind': {
363
+ // The day note is composed of tagged segments; an agent wants the prose.
364
+ const segs = arr(S.moodDayJournals?.[today])
365
+ return {
366
+ today: arr(S.moods?.[today]).map(m => m.mood || m.key),
367
+ day_score: S.dayScores?.[today] || null,
368
+ note: segs.map(x => x.text).filter(Boolean).join(' ').slice(0, 800) || null,
369
+ }
370
+ }
371
+ case 'squares':
372
+ return {
373
+ total: num(S.totalSquares),
374
+ decay: S.decayState || null,
375
+ recent_losses: arr(S.decayLog).slice(-5),
376
+ }
377
+ case 'backlog':
378
+ return arr(S.backlogTasks).slice(0, 40).map(t => ({ text: t.text, horizon: t.horizon || null }))
379
+ case 'missions':
380
+ return arr(S.missions).map(m => ({ title: m.title, done: !!m.done, tasks: arr(m.taskIds).length }))
381
+ default:
382
+ return { error: `unknown area "${area}"`, known: Object.keys(buildBrief(S, now).areas) }
383
+ }
384
+ }
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // ─────────────────────────────────────────────────────────────────────────────
3
- // TASK//SWIPE MCP SERVER — stdio transport.
3
+ // TASKKA MCP SERVER — stdio transport.
4
4
  //
5
5
  // Transport is deliberately the only thing in this file. Tools live in
6
6
  // tools.js and know nothing about MCP; state access lives in store.js and
@@ -51,11 +51,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
51
51
  log(`[taskswipe] ${tool.name} failed:`, e?.message || e)
52
52
  return {
53
53
  isError: true,
54
- content: [{ type: 'text', text: `TASK//SWIPE: ${e?.message || 'unknown error'}` }],
54
+ content: [{ type: 'text', text: `TASKKA: ${e?.message || 'unknown error'}` }],
55
55
  }
56
56
  }
57
57
  })
58
58
 
59
- const transport = new StdioServerTransport()
60
- await server.connect(transport)
61
- log('[taskswipe] MCP server ready on stdio —', TOOLS.length, 'tools')
59
+ if (process.argv[2] === 'setup') {
60
+ const { runSetup } = await import('./setup.js')
61
+ await runSetup(process.argv.slice(3))
62
+ } else {
63
+ const transport = new StdioServerTransport()
64
+ await server.connect(transport)
65
+ log('[taskswipe] MCP server ready on stdio —', TOOLS.length, 'tools')
66
+ }
package/src/ob1.js ADDED
@@ -0,0 +1,104 @@
1
+ // OB1 / Open Brain — durable cross-tool memory (Supabase + MCP).
2
+ // TASKKA talks to the same HTTP MCP Codex and Grok already use.
3
+ // Do not dump the vault into it. Capture a concise lesson, not a file.
4
+
5
+ import { looksLikeSecret } from './os.js'
6
+
7
+ const MAX_CAPTURE = 4000
8
+
9
+ function endpoint() {
10
+ const raw = String(process.env.OPEN_BRAIN_URL || process.env.OB1_MCP_URL || '').trim()
11
+ if (!raw) return ''
12
+ const key = String(process.env.OPEN_BRAIN_KEY || process.env.OB1_KEY || '').trim()
13
+ if (!key || /[?&]key=/.test(raw)) return raw
14
+ return raw.includes('?') ? `${raw}&key=${encodeURIComponent(key)}` : `${raw}?key=${encodeURIComponent(key)}`
15
+ }
16
+
17
+ export function ob1Configured() {
18
+ return !!endpoint()
19
+ }
20
+
21
+ function parseMcpBody(text) {
22
+ const src = String(text || '').trim()
23
+ if (!src) return {}
24
+ if (src.startsWith('{')) {
25
+ try { return JSON.parse(src) } catch { /* SSE below */ }
26
+ }
27
+ for (const block of src.split('\n\n')) {
28
+ const line = block.split('\n').find(l => l.startsWith('data: '))
29
+ if (!line) continue
30
+ try { return JSON.parse(line.slice(6)) } catch { /* next */ }
31
+ }
32
+ return { error: { message: 'ob1_unreadable' } }
33
+ }
34
+
35
+ async function mcpCall(name, args = {}) {
36
+ const url = endpoint()
37
+ if (!url) throw Object.assign(new Error('ob1_unconfigured'), { status: 503 })
38
+ const res = await fetch(url, {
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ Accept: 'application/json, text/event-stream',
43
+ },
44
+ body: JSON.stringify({
45
+ jsonrpc: '2.0',
46
+ id: Date.now(),
47
+ method: 'tools/call',
48
+ params: { name, arguments: args },
49
+ }),
50
+ })
51
+ const raw = await res.text()
52
+ const msg = parseMcpBody(raw)
53
+ if (!res.ok) {
54
+ throw Object.assign(new Error(msg.error?.message || `ob1_${res.status}`), { status: res.status === 401 ? 403 : 502 })
55
+ }
56
+ if (msg.error) {
57
+ throw Object.assign(new Error(msg.error.message || 'ob1_failed'), { status: 502 })
58
+ }
59
+ const text = (msg.result?.content || []).map(c => c.text || '').filter(Boolean).join('\n')
60
+ if (msg.result?.isError) {
61
+ throw Object.assign(new Error(text || 'ob1_tool_error'), { status: 502 })
62
+ }
63
+ return { text, result: msg.result }
64
+ }
65
+
66
+ export function formatOb1Capture({ text, source, title } = {}) {
67
+ const body = String(text || '').trim()
68
+ const head = ['TASKKA', source, title].filter(Boolean).join(' · ')
69
+ const packed = head ? `${head}\n\n${body}` : body
70
+ return packed.slice(0, MAX_CAPTURE)
71
+ }
72
+
73
+ export async function ob1Status() {
74
+ if (!ob1Configured()) return { configured: false, thoughts: 0 }
75
+ try {
76
+ const { text } = await mcpCall('thought_stats', {})
77
+ const total = Number((text.match(/Total thoughts:\s*(\d+)/i) || [])[1] || 0)
78
+ return { configured: true, thoughts: total, summary: text.slice(0, 1200) }
79
+ } catch (e) {
80
+ return { configured: true, thoughts: 0, error: e.message || 'ob1_status_failed' }
81
+ }
82
+ }
83
+
84
+ export async function searchOb1(query, { limit = 8, threshold = 0.5 } = {}) {
85
+ const q = String(query || '').trim()
86
+ if (q.length < 2) return { query: q, results: [] }
87
+ const { text } = await mcpCall('search_thoughts', { query: q, limit, threshold })
88
+ return { query: q, text }
89
+ }
90
+
91
+ export async function listOb1({ limit = 8, days } = {}) {
92
+ const args = { limit }
93
+ if (days) args.days = days
94
+ const { text } = await mcpCall('list_thoughts', args)
95
+ return { text }
96
+ }
97
+
98
+ export async function captureOb1({ text, source, title } = {}) {
99
+ const content = formatOb1Capture({ text, source, title })
100
+ if (!content.trim()) throw Object.assign(new Error('empty_thought'), { status: 400 })
101
+ if (looksLikeSecret(content)) throw Object.assign(new Error('secrets_forbidden'), { status: 400 })
102
+ const { text: reply } = await mcpCall('capture_thought', { content })
103
+ return { ok: true, message: reply || 'Captured in OB1.' }
104
+ }