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 +51 -10
- package/package.json +2 -2
- package/src/brief.js +384 -0
- package/src/index.js +10 -5
- package/src/ob1.js +104 -0
- package/src/os.js +367 -0
- package/src/setup.js +182 -0
- package/src/store.js +63 -20
- package/src/tools.js +361 -7
package/src/tools.js
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
// can undo a batch.
|
|
14
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
15
|
|
|
16
|
-
import { readState, mutate } from './store.js'
|
|
16
|
+
import { readState, mutate, currentUser } from './store.js'
|
|
17
|
+
import { buildBrief, buildDetail } from './brief.js'
|
|
18
|
+
import { isOsOwner, osConfigured, osStatus, readNote, searchNotes, writeNote, appendNote, ensureFrontmatter, notePathFrom } from './os.js'
|
|
19
|
+
import { ob1Configured, ob1Status, searchOb1, captureOb1 } from './ob1.js'
|
|
17
20
|
|
|
18
21
|
const MAX_ADD = 10
|
|
19
22
|
const uid = () => Math.random().toString(36).slice(2, 11)
|
|
@@ -23,6 +26,27 @@ const today = () => {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const arr = (v) => (Array.isArray(v) ? v : [])
|
|
29
|
+
const num = (v) => (Number.isFinite(+v) ? +v : 0)
|
|
30
|
+
|
|
31
|
+
// Mirrors GOAL_FRAMES and creditGoal in src/lib/goals.js. Duplicated rather
|
|
32
|
+
// than imported because this package ships standalone to npm and cannot reach
|
|
33
|
+
// into the app's src/. If the goal model changes there, change it here too.
|
|
34
|
+
const GOAL_FRAMES = ['3month', '6month', 'yearly', '3year', '7year']
|
|
35
|
+
function creditGoal(goals = {}, { goalId, goalFrame, squares }) {
|
|
36
|
+
if (!goalId || !squares) return goals || {}
|
|
37
|
+
const frames = goalFrame && GOAL_FRAMES.includes(goalFrame) ? [goalFrame] : GOAL_FRAMES
|
|
38
|
+
const next = { ...(goals || {}) }
|
|
39
|
+
for (const frame of frames) {
|
|
40
|
+
const list = arr(next[frame])
|
|
41
|
+
const idx = list.findIndex(g => g.id === goalId)
|
|
42
|
+
if (idx < 0) continue
|
|
43
|
+
next[frame] = list.map((g, i) => (i === idx
|
|
44
|
+
? { ...g, earnedBricks: Math.max(0, num(g.earnedBricks) + squares) }
|
|
45
|
+
: g))
|
|
46
|
+
return next
|
|
47
|
+
}
|
|
48
|
+
return goals || {}
|
|
49
|
+
}
|
|
26
50
|
|
|
27
51
|
/** Match a task the way a person refers to it: by substring, not by id. */
|
|
28
52
|
function findTask(list, ref) {
|
|
@@ -43,11 +67,55 @@ function findTask(list, ref) {
|
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
export const TOOLS = [
|
|
70
|
+
// FIRST CALL, ALWAYS. Six tools covering three of twenty-five tabs left an
|
|
71
|
+
// agent guessing what existed and stitching several reads into a picture. One
|
|
72
|
+
// bounded call now describes the whole system — see brief.js for why it
|
|
73
|
+
// summarises rather than serialising.
|
|
74
|
+
{
|
|
75
|
+
name: 'brief',
|
|
76
|
+
title: 'The whole picture, briefly',
|
|
77
|
+
description:
|
|
78
|
+
'START HERE for anything about how the user is doing, what they should do '
|
|
79
|
+
+ 'next, or what is going on in any part of their life. Returns a compact '
|
|
80
|
+
+ 'snapshot of the ENTIRE app — today, what needs attention ranked by '
|
|
81
|
+
+ 'severity, and a one-line state for every area (habits, stacks, goals, '
|
|
82
|
+
+ 'food, lift, walk, mind, study, squares and the rest). About a kilobyte '
|
|
83
|
+
+ 'whether they joined yesterday or two years ago. Follow with detail() '
|
|
84
|
+
+ 'for depth on one area.',
|
|
85
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
86
|
+
async run() {
|
|
87
|
+
const { state } = await readState()
|
|
88
|
+
return buildBrief(state)
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
name: 'detail',
|
|
94
|
+
title: 'One area, in depth',
|
|
95
|
+
description:
|
|
96
|
+
'Depth on ONE area named by brief(). Pass any key from the brief\'s '
|
|
97
|
+
+ '`areas` — today, dropped, backlog, docket, habits, stacks, goals, '
|
|
98
|
+
+ 'food, lift, mind, study, squares, lists, frame, business, people, '
|
|
99
|
+
+ 'things, missions. DROPPED is LOG\'s pile of unfinished tasks from '
|
|
100
|
+
+ 'earlier days. Still summarised: you get what someone would reason '
|
|
101
|
+
+ 'with, not the raw rows.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: { area: { type: 'string', description: 'e.g. "habits", "food", "study"' } },
|
|
105
|
+
required: ['area'],
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
108
|
+
async run({ area }) {
|
|
109
|
+
const { state } = await readState()
|
|
110
|
+
return buildDetail(state, area)
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
46
114
|
{
|
|
47
115
|
name: 'today',
|
|
48
116
|
title: "Today's plan",
|
|
49
117
|
description:
|
|
50
|
-
"What
|
|
118
|
+
"What TASKKA has for today: open and completed tasks, the core task, "
|
|
51
119
|
+ "the day score if posted, and today's timed blocks. Call this before "
|
|
52
120
|
+ "answering anything about what the user is doing today.",
|
|
53
121
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
@@ -130,10 +198,19 @@ export const TOOLS = [
|
|
|
130
198
|
+ 'a few words is enough, no id needed. Returns the exact task matched.',
|
|
131
199
|
inputSchema: {
|
|
132
200
|
type: 'object',
|
|
133
|
-
properties: {
|
|
201
|
+
properties: {
|
|
202
|
+
task: { type: 'string', minLength: 1, description: 'Any part of the task text.' },
|
|
203
|
+
squares: {
|
|
204
|
+
type: 'integer', minimum: 0, maximum: 48,
|
|
205
|
+
description:
|
|
206
|
+
'How many squares the task was worth. Omit unless the user says — '
|
|
207
|
+
+ 'a recurring rule with a preset worth supplies its own, and '
|
|
208
|
+
+ 'anything else is worth 0 until the user values it in the app.',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
134
211
|
required: ['task'], additionalProperties: false,
|
|
135
212
|
},
|
|
136
|
-
async run({ task }) {
|
|
213
|
+
async run({ task, squares }) {
|
|
137
214
|
let result = null
|
|
138
215
|
await mutate(state => {
|
|
139
216
|
const list = arr(state.todayTasks)
|
|
@@ -141,10 +218,52 @@ export const TOOLS = [
|
|
|
141
218
|
if (!hit) { result = { ok: false, reason: 'no_match' }; return null }
|
|
142
219
|
if (hit.ambiguous) { result = { ok: false, reason: 'ambiguous', candidates: hit.ambiguous }; return null }
|
|
143
220
|
if (hit.done) { result = { ok: true, already: true, task: hit.text }; return null }
|
|
144
|
-
|
|
221
|
+
|
|
222
|
+
// Mirrors the COMPLETE_TASK reducer in src/store.jsx. Ticking a task is
|
|
223
|
+
// not just a flag: it earns squares, writes a history entry, feeds the
|
|
224
|
+
// level counter, and credits any linked stack or goal. Setting `done`
|
|
225
|
+
// alone — which is all this did — silently dropped every one of those,
|
|
226
|
+
// so a recurring task configured as worth 8 earned nothing when ticked
|
|
227
|
+
// from here. Keep the two in step.
|
|
228
|
+
//
|
|
229
|
+
// Coerce like the reducer does: one non-finite value would make
|
|
230
|
+
// totalSquares NaN, and NaN survives every later addition, quietly
|
|
231
|
+
// destroying the currency for good.
|
|
232
|
+
const asked = squares !== undefined ? squares : hit.presetSquares
|
|
233
|
+
const earned = Number.isFinite(+asked) ? Math.max(0, +asked) : 0
|
|
234
|
+
|
|
235
|
+
const newToday = list.map(t => (t.id === hit.id
|
|
236
|
+
? {
|
|
237
|
+
...t, done: true, completedAt: Date.now(),
|
|
238
|
+
// Settling the parent settles its steps, as the app does.
|
|
239
|
+
...(t.subtasks ? { subtasks: t.subtasks.map(sub => ({ ...sub, done: true })) } : {}),
|
|
240
|
+
}
|
|
241
|
+
: t))
|
|
242
|
+
|
|
243
|
+
const entry = {
|
|
244
|
+
...hit, done: true, squares: earned,
|
|
245
|
+
stackId: hit.stackId, notes: '', at: Date.now(), source: 'mcp',
|
|
246
|
+
}
|
|
247
|
+
if (!entry.goalId) { delete entry.goalId; delete entry.goalFrame }
|
|
248
|
+
|
|
249
|
+
const newTotal = num(state.totalSquares) + earned
|
|
250
|
+
const leveledUp = earned > 0
|
|
251
|
+
&& Math.floor(num(state.totalSquares) / 48) !== Math.floor(newTotal / 48)
|
|
252
|
+
|
|
253
|
+
const newStacks = hit.stackId
|
|
254
|
+
? arr(state.stacks).map(st => (st.id === hit.stackId
|
|
255
|
+
? { ...st, completions: [...arr(st.completions), { date: today(), task: hit.text }] }
|
|
256
|
+
: st))
|
|
257
|
+
: state.stacks
|
|
258
|
+
|
|
259
|
+
result = { ok: true, task: hit.text, squares: earned, total: newTotal, leveledUp }
|
|
145
260
|
return {
|
|
146
261
|
...state,
|
|
147
|
-
todayTasks:
|
|
262
|
+
todayTasks: newToday,
|
|
263
|
+
achievedTasks: [entry, ...arr(state.achievedTasks)],
|
|
264
|
+
totalSquares: newTotal,
|
|
265
|
+
stacks: newStacks,
|
|
266
|
+
goals: earned > 0 ? creditGoal(state.goals, { goalId: entry.goalId, goalFrame: entry.goalFrame, squares: earned }) : state.goals,
|
|
148
267
|
}
|
|
149
268
|
})
|
|
150
269
|
if (!result?.ok && result?.reason === 'no_match') {
|
|
@@ -153,7 +272,15 @@ export const TOOLS = [
|
|
|
153
272
|
if (result?.reason === 'ambiguous') {
|
|
154
273
|
return { ok: false, message: `That matches several: ${result.candidates.join(', ')}. Be more specific.` }
|
|
155
274
|
}
|
|
156
|
-
return { ok: true, message:
|
|
275
|
+
if (result.already) return { ok: true, message: `"${result.task}" was already done.` }
|
|
276
|
+
const earned = result.squares
|
|
277
|
+
? ` +${result.squares} square${result.squares === 1 ? '' : 's'} (${result.total} total)`
|
|
278
|
+
: ''
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
message: `Ticked off "${result.task}"${earned}.`
|
|
282
|
+
+ (result.leveledUp ? ' That completed a brick — level up.' : ''),
|
|
283
|
+
}
|
|
157
284
|
},
|
|
158
285
|
},
|
|
159
286
|
|
|
@@ -201,6 +328,51 @@ export const TOOLS = [
|
|
|
201
328
|
},
|
|
202
329
|
},
|
|
203
330
|
|
|
331
|
+
{
|
|
332
|
+
name: 'recover_dropped',
|
|
333
|
+
title: 'Put a DROPPED task back on today',
|
|
334
|
+
description:
|
|
335
|
+
'LOG → DROPPED holds tasks that aged out unfinished from earlier days. '
|
|
336
|
+
+ 'This puts one back on today. Refer to it by any part of its text. '
|
|
337
|
+
+ 'Does not charge decay. Use detail({ area: "dropped" }) first if you '
|
|
338
|
+
+ 'do not know the wording.',
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: 'object',
|
|
341
|
+
properties: {
|
|
342
|
+
task: { type: 'string', minLength: 1, description: 'Any part of the dropped task text.' },
|
|
343
|
+
},
|
|
344
|
+
required: ['task'],
|
|
345
|
+
additionalProperties: false,
|
|
346
|
+
},
|
|
347
|
+
async run({ task }) {
|
|
348
|
+
let result = null
|
|
349
|
+
await mutate(state => {
|
|
350
|
+
const list = arr(state.missedTasks)
|
|
351
|
+
const hit = findTask(list, task)
|
|
352
|
+
if (!hit) { result = { ok: false, reason: 'no_match' }; return null }
|
|
353
|
+
if (hit.ambiguous) { result = { ok: false, reason: 'ambiguous', candidates: hit.ambiguous }; return null }
|
|
354
|
+
const { expiredDate, ...rest } = hit
|
|
355
|
+
const already = arr(state.todayTasks).find(t => !t.done && (t.text || '').toLowerCase() === (rest.text || '').toLowerCase())
|
|
356
|
+
result = { ok: true, task: rest.text, already: !!already }
|
|
357
|
+
return {
|
|
358
|
+
...state,
|
|
359
|
+
missedTasks: list.filter(t => t.id !== hit.id),
|
|
360
|
+
todayTasks: already
|
|
361
|
+
? state.todayTasks
|
|
362
|
+
: [...arr(state.todayTasks), { ...rest, id: uid(), done: false, createdDate: today(), source: 'mcp' }],
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
if (result?.reason === 'no_match') {
|
|
366
|
+
return { ok: false, message: `Nothing in DROPPED matches "${task}". Call detail({ area: "dropped" }).` }
|
|
367
|
+
}
|
|
368
|
+
if (result?.reason === 'ambiguous') {
|
|
369
|
+
return { ok: false, message: `That matches several in DROPPED: ${result.candidates.join(', ')}. Be more specific.` }
|
|
370
|
+
}
|
|
371
|
+
if (result.already) return { ok: true, message: `"${result.task}" was already on today. Removed from DROPPED.` }
|
|
372
|
+
return { ok: true, message: `Put "${result.task}" back on today from DROPPED.` }
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
|
|
204
376
|
{
|
|
205
377
|
name: 'search',
|
|
206
378
|
title: 'Search everything',
|
|
@@ -224,9 +396,14 @@ export const TOOLS = [
|
|
|
224
396
|
if (text && String(text).toLowerCase().includes(q)) hits.push({ where, text: String(text), ...extra })
|
|
225
397
|
}
|
|
226
398
|
arr(state.todayTasks).forEach(t => add('today', t.text, { done: !!t.done }))
|
|
399
|
+
arr(state.missedTasks).forEach(t => add('dropped', t.text, { expired: t.expiredDate || null }))
|
|
400
|
+
arr(state.cutTasks).forEach(t => add('cut', t.text))
|
|
227
401
|
arr(state.backlogTasks).forEach(t => add('backlog', t.text))
|
|
402
|
+
arr(state.docketTasks).forEach(t => add('docket', t.text || t.task))
|
|
228
403
|
arr(state.taskDatabase).forEach(t => add('database', t.text))
|
|
229
404
|
arr(state.ideas).forEach(i => add('ideas', i.text))
|
|
405
|
+
arr(state.businessIdeas).forEach(i => add('business', i.title, { stage: i.stage || 'spark' }))
|
|
406
|
+
arr(state.people).forEach(p => add('people', p.name, { role: p.role || null }))
|
|
230
407
|
arr(state.decompress).forEach(d => add('decompress', d.text))
|
|
231
408
|
Object.entries(state.journal || {}).forEach(([date, entries]) =>
|
|
232
409
|
arr(entries).forEach(e => add('journal', e?.text, { date })))
|
|
@@ -237,6 +414,183 @@ export const TOOLS = [
|
|
|
237
414
|
return { query, count: hits.length, results: hits.slice(0, limit) }
|
|
238
415
|
},
|
|
239
416
|
},
|
|
417
|
+
|
|
418
|
+
{
|
|
419
|
+
name: 'os_search',
|
|
420
|
+
title: 'Search Callum OS',
|
|
421
|
+
description:
|
|
422
|
+
'Search Callum OS, the life wiki: businesses, portfolio, knowledge, '
|
|
423
|
+
+ 'life areas and decisions. Use this when the question is about a '
|
|
424
|
+
+ 'company, a written decision, or anything that lives in the vault '
|
|
425
|
+
+ 'rather than today\'s TASKKA tasks.',
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: 'object',
|
|
428
|
+
properties: {
|
|
429
|
+
query: { type: 'string', minLength: 2 },
|
|
430
|
+
limit: { type: 'integer', minimum: 1, maximum: 40, default: 12 },
|
|
431
|
+
},
|
|
432
|
+
required: ['query'],
|
|
433
|
+
additionalProperties: false,
|
|
434
|
+
},
|
|
435
|
+
async run({ query, limit = 12 }) {
|
|
436
|
+
const user = await currentUser()
|
|
437
|
+
if (!isOsOwner(user)) throw new Error('Callum OS is locked to the vault owner.')
|
|
438
|
+
if (!osConfigured()) throw new Error('Callum OS is not configured on this server.')
|
|
439
|
+
return searchNotes(query, limit)
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
{
|
|
444
|
+
name: 'os_read',
|
|
445
|
+
title: 'Read a Callum OS note',
|
|
446
|
+
description:
|
|
447
|
+
'Read one markdown note from Callum OS by path, e.g. "index.md", '
|
|
448
|
+
+ '"10 Businesses/Care in Movement/index.md", or "60 Life/index.md". '
|
|
449
|
+
+ 'Call os_search first if you do not know the path.',
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: 'object',
|
|
452
|
+
properties: { path: { type: 'string', minLength: 1, maxLength: 400 } },
|
|
453
|
+
required: ['path'],
|
|
454
|
+
additionalProperties: false,
|
|
455
|
+
},
|
|
456
|
+
async run({ path }) {
|
|
457
|
+
const user = await currentUser()
|
|
458
|
+
if (!isOsOwner(user)) throw new Error('Callum OS is locked to the vault owner.')
|
|
459
|
+
if (!osConfigured()) throw new Error('Callum OS is not configured on this server.')
|
|
460
|
+
const note = await readNote(path)
|
|
461
|
+
const text = String(note.text || '')
|
|
462
|
+
return { ...note, text: text.length > 12000 ? `${text.slice(0, 12000)}\n…[truncated]` : text }
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
{
|
|
467
|
+
name: 'os_status',
|
|
468
|
+
title: 'Callum OS connection',
|
|
469
|
+
description: 'Whether Callum OS is reachable and how many notes are in the vault.',
|
|
470
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
471
|
+
async run() {
|
|
472
|
+
const user = await currentUser()
|
|
473
|
+
if (!isOsOwner(user)) throw new Error('Callum OS is locked to the vault owner.')
|
|
474
|
+
return osStatus()
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
{
|
|
479
|
+
name: 'os_write',
|
|
480
|
+
title: 'Write a Callum OS note',
|
|
481
|
+
description:
|
|
482
|
+
'Create or replace one markdown note in Callum OS. Pass the full vault '
|
|
483
|
+
+ 'path (e.g. "01 Inbox/capture.md" or "10 Businesses/Care in Movement/notes.md"). '
|
|
484
|
+
+ 'This OVERWRITES the file — call os_read first if you are editing. '
|
|
485
|
+
+ 'New notes get YAML frontmatter with type: Note. Never write secrets.',
|
|
486
|
+
inputSchema: {
|
|
487
|
+
type: 'object',
|
|
488
|
+
properties: {
|
|
489
|
+
path: { type: 'string', minLength: 1, maxLength: 400 },
|
|
490
|
+
text: { type: 'string', minLength: 1, maxLength: 50000 },
|
|
491
|
+
message: { type: 'string', maxLength: 200 },
|
|
492
|
+
folder: { type: 'string', description: 'Optional folder when creating from a title instead of a path' },
|
|
493
|
+
title: { type: 'string', description: 'Optional title used to build a new path under folder' },
|
|
494
|
+
},
|
|
495
|
+
additionalProperties: false,
|
|
496
|
+
},
|
|
497
|
+
async run({ path, text, message, folder, title }) {
|
|
498
|
+
const user = await currentUser()
|
|
499
|
+
if (!isOsOwner(user)) throw new Error('Callum OS is locked to the vault owner.')
|
|
500
|
+
if (!osConfigured()) throw new Error('Callum OS is not configured on this server.')
|
|
501
|
+
const target = path || notePathFrom({ folder: folder || '01 Inbox', name: title || 'Note' })
|
|
502
|
+
if (!target) throw new Error('Need a path or a title for the new note.')
|
|
503
|
+
const body = ensureFrontmatter(text, { title: title || target.split('/').pop() })
|
|
504
|
+
const saved = await writeNote({ path: target, text: body, message })
|
|
505
|
+
return { ok: true, ...saved, message: saved.created ? `Created ${saved.path}` : `Updated ${saved.path}` }
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
{
|
|
510
|
+
name: 'os_append',
|
|
511
|
+
title: 'Append to a Callum OS note',
|
|
512
|
+
description:
|
|
513
|
+
'Add text to the end of an existing Callum OS note (creates it if missing). '
|
|
514
|
+
+ 'Use this for log.md, inbox capture, or adding a decision without rewriting the file.',
|
|
515
|
+
inputSchema: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
properties: {
|
|
518
|
+
path: { type: 'string', minLength: 1, maxLength: 400 },
|
|
519
|
+
text: { type: 'string', minLength: 1, maxLength: 20000 },
|
|
520
|
+
message: { type: 'string', maxLength: 200 },
|
|
521
|
+
},
|
|
522
|
+
required: ['path', 'text'],
|
|
523
|
+
additionalProperties: false,
|
|
524
|
+
},
|
|
525
|
+
async run({ path, text, message }) {
|
|
526
|
+
const user = await currentUser()
|
|
527
|
+
if (!isOsOwner(user)) throw new Error('Callum OS is locked to the vault owner.')
|
|
528
|
+
if (!osConfigured()) throw new Error('Callum OS is not configured on this server.')
|
|
529
|
+
const saved = await appendNote({ path, text, message })
|
|
530
|
+
return { ok: true, ...saved, message: `Appended to ${saved.path}` }
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
{
|
|
535
|
+
name: 'ob1_status',
|
|
536
|
+
title: 'OB1 connection',
|
|
537
|
+
description: 'Whether One Brain / OB1 is reachable and how many thoughts it holds.',
|
|
538
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
539
|
+
async run() {
|
|
540
|
+
const user = await currentUser()
|
|
541
|
+
if (!isOsOwner(user)) throw new Error('OB1 is locked to the vault owner.')
|
|
542
|
+
return ob1Status()
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
{
|
|
547
|
+
name: 'ob1_search',
|
|
548
|
+
title: 'Search OB1',
|
|
549
|
+
description:
|
|
550
|
+
'Search One Brain / OB1, the durable cross-tool memory. Use this for '
|
|
551
|
+
+ 'preferences, lessons, and decisions that should follow Callum between '
|
|
552
|
+
+ 'apps — not for today\'s TASKKA tasks or a whole Callum OS note.',
|
|
553
|
+
inputSchema: {
|
|
554
|
+
type: 'object',
|
|
555
|
+
properties: {
|
|
556
|
+
query: { type: 'string', minLength: 2 },
|
|
557
|
+
limit: { type: 'integer', minimum: 1, maximum: 20, default: 8 },
|
|
558
|
+
},
|
|
559
|
+
required: ['query'],
|
|
560
|
+
additionalProperties: false,
|
|
561
|
+
},
|
|
562
|
+
async run({ query, limit = 8 }) {
|
|
563
|
+
const user = await currentUser()
|
|
564
|
+
if (!isOsOwner(user)) throw new Error('OB1 is locked to the vault owner.')
|
|
565
|
+
if (!ob1Configured()) throw new Error('OB1 is not configured on this server.')
|
|
566
|
+
return searchOb1(query, { limit })
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
{
|
|
571
|
+
name: 'ob1_capture',
|
|
572
|
+
title: 'Capture to OB1',
|
|
573
|
+
description:
|
|
574
|
+
'Save one concise thought to One Brain / OB1. Use this when a map topic, '
|
|
575
|
+
+ 'Parley card, learn, or note should follow Callum into future AI sessions. '
|
|
576
|
+
+ 'Do not dump a whole vault file. Never write secrets.',
|
|
577
|
+
inputSchema: {
|
|
578
|
+
type: 'object',
|
|
579
|
+
properties: {
|
|
580
|
+
text: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
581
|
+
source: { type: 'string', maxLength: 40, description: 'parley, study-map, note, idea, os' },
|
|
582
|
+
title: { type: 'string', maxLength: 160 },
|
|
583
|
+
},
|
|
584
|
+
required: ['text'],
|
|
585
|
+
additionalProperties: false,
|
|
586
|
+
},
|
|
587
|
+
async run({ text, source, title }) {
|
|
588
|
+
const user = await currentUser()
|
|
589
|
+
if (!isOsOwner(user)) throw new Error('OB1 is locked to the vault owner.')
|
|
590
|
+
if (!ob1Configured()) throw new Error('OB1 is not configured on this server.')
|
|
591
|
+
return captureOb1({ text, source, title })
|
|
592
|
+
},
|
|
593
|
+
},
|
|
240
594
|
]
|
|
241
595
|
|
|
242
596
|
export const byName = Object.fromEntries(TOOLS.map(t => [t.name, t]))
|