← Public packages
@kody/personal-capture
Quick onboarding example: stash notes/links in packageStorage; list recent captures.
src/index.ts
155 lines · 4.3 KB · TypeScriptimport { kody, packageStorage } from 'kody:runtime'
const INDEX_KEY = 'captures:index'
const ITEM_PREFIX = 'captures:item:'
export type CaptureInput = {
/** Note text to stash. */
text: string
/** Optional related URL. */
url?: string
/** When true, email yourself a short confirmation via kody.email_send. */
notify?: boolean
}
export type ListCapturesInput = {
/** Max items to return (1–50, default 20). */
limit?: number
}
export type CaptureItem = {
id: string
text: string
url: string | null
createdAt: string
}
function requireText(text: unknown): string {
if (typeof text !== 'string' || text.trim() === '') {
throw new Error('text is required.')
}
return text.trim()
}
function clampLimit(limit: unknown, fallback: number): number {
const n = Number(limit)
if (!Number.isFinite(n)) return fallback
return Math.min(50, Math.max(1, Math.floor(n)))
}
function newId(): string {
return (
Date.now().toString(36) +
'-' +
Math.random().toString(36).slice(2, 10)
)
}
async function readIndex(store: ReturnType<typeof packageStorage>): Promise<string[]> {
const raw = await store.get(INDEX_KEY)
if (!raw) return []
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
return Array.isArray(parsed) ? parsed.map(String) : []
} catch {
return []
}
}
/**
* Save a note/link into this package's packageStorage bucket.
* Prefer the `./capture` export from callers; this named helper is shared with the root dispatcher.
*
* @param input.text - Note text to stash (required).
* @param input.url - Optional related URL.
* @param input.notify - When true, email a short confirmation.
* @returns The stored capture item.
*
* @example
* import { capture } from 'kody:@kody/personal-capture/capture'
* const item = await capture({ text: 'Follow up on Sentry triage' })
*/
export async function capture(input: CaptureInput): Promise<CaptureItem> {
const text = requireText(input?.text)
const url =
typeof input?.url === 'string' && input.url.trim()
? input.url.trim()
: null
const store = packageStorage()
const item: CaptureItem = {
id: newId(),
text,
url,
createdAt: new Date().toISOString(),
}
const index = await readIndex(store)
index.unshift(item.id)
// Keep the index bounded so the example stays tiny.
const trimmed = index.slice(0, 200)
await store.set(ITEM_PREFIX + item.id, JSON.stringify(item))
await store.set(INDEX_KEY, JSON.stringify(trimmed))
if (input?.notify === true) {
const subject = 'Captured: ' + item.text.slice(0, 80)
const lines = [
item.text,
item.url ? 'URL: ' + item.url : null,
'id: ' + item.id,
'at: ' + item.createdAt,
].filter(Boolean)
await kody.emailSend({ subject, text: lines.join('\n') })
}
return item
}
/**
* List recent captures newest first from packageStorage.
* Prefer the `./listCaptures` export from callers.
*
* @param input.limit - Max items to return (1–50, default 20).
* @returns Recent capture items newest first.
*
* @example
* import { listCaptures } from 'kody:@kody/personal-capture/listCaptures'
* const items = await listCaptures({ limit: 10 })
*/
export async function listCaptures(
input: ListCapturesInput = {},
): Promise<CaptureItem[]> {
const limit = clampLimit(input?.limit, 20)
const store = packageStorage()
const index = await readIndex(store)
const ids = index.slice(0, limit)
const items: CaptureItem[] = []
for (const id of ids) {
const raw = await store.get(ITEM_PREFIX + id)
if (!raw) continue
try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
if (parsed && typeof parsed === 'object' && parsed.id) {
items.push({
id: String(parsed.id),
text: String(parsed.text || ''),
url: parsed.url ? String(parsed.url) : null,
createdAt: String(parsed.createdAt || ''),
})
}
} catch {
// skip corrupt rows in this tiny example
}
}
return items
}
/** Root dispatcher for packages.invoke({ exportName: '.', params }). */
export default async function personalCapture(
input: (CaptureInput & { action?: string }) | (ListCapturesInput & { action?: string }) = { text: '' },
) {
const action = String((input as { action?: string })?.action || 'capture')
if (action === 'listCaptures' || action === 'list') {
return listCaptures(input as ListCapturesInput)
}
return capture(input as CaptureInput)
}