@tekmidian/pai 0.12.2 → 0.13.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.
Files changed (45) hide show
  1. package/dist/cli/index.mjs +2 -2
  2. package/dist/cli/program.d.mts.map +1 -1
  3. package/dist/cli/program.mjs +3 -2
  4. package/dist/cli/program.mjs.map +1 -1
  5. package/dist/{config-BuhHWyOK.mjs → config-C8m-tPhP.mjs} +22 -2
  6. package/dist/config-C8m-tPhP.mjs.map +1 -0
  7. package/dist/daemon/index.mjs +2 -2
  8. package/dist/{daemon-Ca7YXHUW.mjs → daemon-D2aKb4mJ.mjs} +2 -2
  9. package/dist/{daemon-Ca7YXHUW.mjs.map → daemon-D2aKb4mJ.mjs.map} +1 -1
  10. package/dist/daemon-mcp/index.mjs +581 -2
  11. package/dist/daemon-mcp/index.mjs.map +1 -1
  12. package/dist/{pick-C5sUQ5g_.mjs → pick-CS_TmOqX.mjs} +1082 -445
  13. package/dist/pick-CS_TmOqX.mjs.map +1 -0
  14. package/dist/skills/Tasks/SKILL.md +101 -0
  15. package/docs/commands/README.md +6 -0
  16. package/docs/commands/backup.md +1 -1
  17. package/docs/commands/clear-names.md +1 -1
  18. package/docs/commands/daemon.md +1 -1
  19. package/docs/commands/db.md +1 -1
  20. package/docs/commands/end.md +1 -1
  21. package/docs/commands/help.md +1 -1
  22. package/docs/commands/kg.md +1 -1
  23. package/docs/commands/mcp.md +1 -1
  24. package/docs/commands/memory.md +1 -1
  25. package/docs/commands/notify.md +1 -1
  26. package/docs/commands/observation.md +1 -1
  27. package/docs/commands/obsidian.md +1 -1
  28. package/docs/commands/pause.md +1 -1
  29. package/docs/commands/project.md +1 -1
  30. package/docs/commands/projects.md +1 -1
  31. package/docs/commands/registry.md +1 -1
  32. package/docs/commands/restore.md +1 -1
  33. package/docs/commands/sessions.md +1 -1
  34. package/docs/commands/setup.md +1 -1
  35. package/docs/commands/shell-init.md +1 -1
  36. package/docs/commands/skill.md +1 -1
  37. package/docs/commands/task.md +90 -0
  38. package/docs/commands/topic.md +1 -1
  39. package/docs/commands/update.md +1 -1
  40. package/docs/commands/zettel.md +1 -1
  41. package/package.json +1 -1
  42. package/plugins/productivity/plugin.json +2 -2
  43. package/plugins/productivity/skills/Tasks/SKILL.md +126 -0
  44. package/dist/config-BuhHWyOK.mjs.map +0 -1
  45. package/dist/pick-C5sUQ5g_.mjs.map +0 -1
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: Tasks
3
+ description: "Run the task bus — read cross-session work from the tracker, dispatch it to the sessions that own it, and file new findings. USE WHEN user says 'morning routine', 'what needs doing', 'run the routine', 'check my tasks', 'dispatch tasks', 'file a finding', 'add this to todoist', 'what did we find', OR /tasks."
4
+ ---
5
+
6
+ ## Tasks Skill
7
+
8
+ USE WHEN user says 'morning routine', 'what needs doing', 'run the routine', 'check my tasks', 'dispatch tasks', 'file a finding', 'add this to todoist', 'what did we find', OR /tasks.
9
+
10
+ ### What This Skill Does
11
+
12
+ The task bus is shared state that outlives any one session. Session-local work stays in each project's `Notes/TODO.md`; the bus carries work that is **broader than one session** — findings discovered sideways, and work that belongs to a project other than the one you are sitting in.
13
+
14
+ Three workflows: **morning routine**, **file a finding**, **triage**.
15
+
16
+ ### Pre-Action Check (MANDATORY)
17
+
18
+ Run this before anything else. If it reports the bus is not configured, say so and stop — do not attempt to work around it by calling the Todoist MCP directly.
19
+
20
+ ```bash
21
+ pai task list --limit 1
22
+ ```
23
+
24
+ If it prints "Task bus is not configured", tell the user to run `pai setup` and complete the Task Bus step. Nothing else in this skill will work.
25
+
26
+ ---
27
+
28
+ ## Workflow 1 — Morning Routine
29
+
30
+ **Trigger:** "morning routine", "what needs doing", "run the routine".
31
+
32
+ ### Step 1 — Read what is open
33
+
34
+ ```bash
35
+ pai task list --today
36
+ ```
37
+
38
+ This shows tasks due today or overdue. Drop `--today` for everything open.
39
+
40
+ ### Step 2 — Report before acting
41
+
42
+ Group what you found by owner and present it. Lead with anything overdue or `p1`.
43
+
44
+ Do **not** dispatch silently. The user should see the list before sessions start receiving work.
45
+
46
+ ### Step 3 — Dispatch
47
+
48
+ ```bash
49
+ pai task dispatch --today --dry-run # always preview first
50
+ pai task dispatch --today # then, once the user approves
51
+ ```
52
+
53
+ Outcomes and what each means:
54
+
55
+ | Symbol | Outcome | Meaning |
56
+ |---|---|---|
57
+ | `→` | delivered | Sent to an already-running session |
58
+ | `+` | spawned | No session was running; one was launched, then sent to |
59
+ | `?` | unrouted | No owner resolved — the task stays in the findings inbox |
60
+ | `!` | unlaunchable | An owner resolved but it has no PAI alias to launch |
61
+ | `·` | skipped | Auto-dispatch is off, or no transport — reported only |
62
+
63
+ **On `!` unlaunchable:** the fix is `pai project name <identifier> <shortname>`. Only projects with a curated alias can be dispatched to. Say this rather than reporting a vague failure.
64
+
65
+ **On `?` unrouted:** this is normal, not an error. It means triage is pending — see Workflow 3.
66
+
67
+ ### Step 4 — Report honestly
68
+
69
+ State what actually reached a session and what did not. If nothing was dispatched because auto-dispatch is off, say that plainly — do not imply work was handed off when it was not.
70
+
71
+ ---
72
+
73
+ ## Workflow 2 — File a Finding
74
+
75
+ **Trigger:** "file a finding", "add this to todoist", or **any time you discover something outside the current task**.
76
+
77
+ This is the point of the bus. When you notice something while doing something else, file it rather than only mentioning it in passing — mentions get lost.
78
+
79
+ ```bash
80
+ pai task add "Short actionable title" \
81
+ --owner <project> \
82
+ --body "Full procedure AND reasoning" \
83
+ --priority p2 \
84
+ --url "hook://..."
85
+ ```
86
+
87
+ ### Rules
88
+
89
+ - **`--body` is not optional in practice.** Put the full procedure and the reasoning in it — enough that the task is actionable months later, or by the user alone, without re-deriving anything. A title alone is a reminder that something was once known, which is worthless later.
90
+ - **Omit `--owner` when you genuinely do not know.** The task lands in the findings inbox for triage. Guessing an owner is worse than leaving it unrouted.
91
+ - **Prefer `hook://` URLs over file paths** for `--url`. Get one with `mcp__hook__hookmark_link`. They survive renames and moves, and open in DEVONthink To Go on iOS. Fall back to a plain path if Hookmark is unavailable.
92
+ - **Do not file generic checklist items.** A recurring item earns its place only because something *actually failed silently*. Otherwise the routine becomes noise and gets ignored.
93
+
94
+ ---
95
+
96
+ ## Workflow 3 — Triage
97
+
98
+ **Trigger:** "triage", "what did we find", or weekly as part of the routine.
99
+
100
+ ```bash
101
+ pai task list --limit 100
102
+ ```
103
+
104
+ Look at everything marked `unrouted`. For each, decide with the user: assign an owner, act on it now, or delete it.
105
+
106
+ Assigning an owner means adding a `pai:<project>` label on the tracker. The label is authoritative and beats the container the task sits in, so it survives being moved.
107
+
108
+ **The findings inbox should normally be empty.** If it is filling up, the bus has become another place for items to rot — say so. That is the failure mode worth naming out loud, and it is a judgement about the system, not about the user.
109
+
110
+ ---
111
+
112
+ ## Ownership — How Tasks Find Their Session
113
+
114
+ Resolution order:
115
+
116
+ 1. A `pai:<project>` label — **authoritative**
117
+ 2. The enclosing sub-project name matched against PAI aliases — fallback
118
+ 3. Otherwise unrouted
119
+
120
+ An explicit label that matches nothing does **not** fall through to the container. That is deliberate: the user meant to route it somewhere specific, and quietly sending it elsewhere would be worse than not routing it at all.
121
+
122
+ ## Notes
123
+
124
+ - `pai task done <id>` closes a task on the tracker. Dispatched tasks tell the receiving session to do this, so work is not dispatched twice.
125
+ - The bus never reads the tracker back into PAI state. It is one-way: PAI and its sessions write, a routine reads.
126
+ - Full architecture and the verified API constraints: `Notes/docs/task-bus.md`.
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-BuhHWyOK.mjs","names":[],"sources":["../src/notifications/types.ts","../src/daemon/config.ts"],"sourcesContent":["/**\n * types.ts — Unified Notification Framework type definitions\n *\n * Defines the channel registry, event routing, and configuration schema\n * for PAI's notification subsystem.\n */\n\n// ---------------------------------------------------------------------------\n// Channel identifiers\n// ---------------------------------------------------------------------------\n\nexport type ChannelId = \"ntfy\" | \"whatsapp\" | \"macos\" | \"voice\" | \"cli\";\n\n// ---------------------------------------------------------------------------\n// Notification event types\n// ---------------------------------------------------------------------------\n\n/**\n * The semantic type of a notification event.\n * Used to route events to the appropriate channels.\n */\nexport type NotificationEvent =\n | \"error\"\n | \"progress\"\n | \"completion\"\n | \"info\"\n | \"debug\";\n\n// ---------------------------------------------------------------------------\n// Notification mode\n// ---------------------------------------------------------------------------\n\n/**\n * The current notification mode.\n *\n * - \"auto\" — Use the per-event routing table (default)\n * - \"voice\" — All events go to voice (WhatsApp TTS)\n * - \"whatsapp\" — All events go to WhatsApp text\n * - \"ntfy\" — All events go to ntfy.sh\n * - \"macos\" — All events go to macOS notifications\n * - \"cli\" — All events go to CLI stdout only\n * - \"off\" — Suppress all notifications\n */\nexport type NotificationMode =\n | \"auto\"\n | \"voice\"\n | \"whatsapp\"\n | \"ntfy\"\n | \"macos\"\n | \"cli\"\n | \"off\";\n\n// ---------------------------------------------------------------------------\n// Per-channel configuration\n// ---------------------------------------------------------------------------\n\nexport interface NtfyChannelConfig {\n enabled: boolean;\n /** ntfy.sh topic URL, e.g. \"https://ntfy.sh/my-topic\" */\n url?: string;\n /** ntfy priority: min | low | default | high | urgent */\n priority?: \"min\" | \"low\" | \"default\" | \"high\" | \"urgent\";\n}\n\nexport interface WhatsAppChannelConfig {\n enabled: boolean;\n /** Optional recipient (phone, JID, or contact name). Omit for self-chat. */\n recipient?: string;\n}\n\nexport interface MacOsChannelConfig {\n enabled: boolean;\n}\n\nexport interface VoiceChannelConfig {\n enabled: boolean;\n /** Kokoro voice name, e.g. \"bm_george\", \"af_bella\". Default: \"bm_george\" */\n voiceName?: string;\n}\n\nexport interface CliChannelConfig {\n enabled: boolean;\n}\n\nexport interface ChannelConfigs {\n ntfy: NtfyChannelConfig;\n whatsapp: WhatsAppChannelConfig;\n macos: MacOsChannelConfig;\n voice: VoiceChannelConfig;\n cli: CliChannelConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Routing table\n// ---------------------------------------------------------------------------\n\n/**\n * Maps each event type to the ordered list of channels that should receive it.\n * Only channels that are enabled in `channels` and present in this list are used.\n */\nexport type RoutingTable = {\n [K in NotificationEvent]: ChannelId[];\n};\n\nexport const DEFAULT_ROUTING: RoutingTable = {\n error: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n completion: [\"whatsapp\", \"macos\", \"ntfy\", \"cli\"],\n info: [\"cli\"],\n progress: [\"cli\"],\n debug: [],\n};\n\n// ---------------------------------------------------------------------------\n// Top-level notification config (embedded in PaiDaemonConfig)\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfig {\n /** Current routing mode. Default: \"auto\" */\n mode: NotificationMode;\n /** Per-channel configuration */\n channels: ChannelConfigs;\n /** Event → channel routing (used in \"auto\" mode) */\n routing: RoutingTable;\n}\n\nexport const DEFAULT_CHANNELS: ChannelConfigs = {\n ntfy: {\n enabled: false,\n url: undefined,\n priority: \"default\",\n },\n whatsapp: {\n enabled: true,\n recipient: undefined,\n },\n macos: {\n enabled: true,\n },\n voice: {\n enabled: false,\n voiceName: \"bm_george\",\n },\n cli: {\n enabled: true,\n },\n};\n\nexport const DEFAULT_NOTIFICATION_CONFIG: NotificationConfig = {\n mode: \"auto\",\n channels: DEFAULT_CHANNELS,\n routing: DEFAULT_ROUTING,\n};\n\n// ---------------------------------------------------------------------------\n// Notification payload\n// ---------------------------------------------------------------------------\n\nexport interface NotificationPayload {\n /** Semantic event type — used for routing */\n event: NotificationEvent;\n /** The notification message body */\n message: string;\n /** Optional title (used by macOS, ntfy) */\n title?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Provider interface\n// ---------------------------------------------------------------------------\n\nexport interface NotificationProvider {\n readonly channelId: ChannelId;\n /**\n * Send a notification.\n * Returns true on success, false on failure (failure is non-fatal).\n */\n send(payload: NotificationPayload, config: NotificationConfig): Promise<boolean>;\n}\n\n// ---------------------------------------------------------------------------\n// Send result\n// ---------------------------------------------------------------------------\n\nexport interface SendResult {\n channelsAttempted: ChannelId[];\n channelsSucceeded: ChannelId[];\n channelsFailed: ChannelId[];\n mode: NotificationMode;\n}\n","/**\n * config.ts — Configuration loader for PAI Daemon\n *\n * Loads config from ~/.config/pai/config.json (XDG convention).\n * Deep-merges with defaults so partial configs work fine.\n * Expands ~ in path values at runtime.\n */\n\nimport { existsSync, readFileSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir, userInfo } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { NotificationConfig } from \"../notifications/types.js\";\nimport { DEFAULT_NOTIFICATION_CONFIG } from \"../notifications/types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchConfig {\n /** Default search mode: 'keyword', 'semantic', or 'hybrid'. Default: 'keyword'. */\n mode: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Enable cross-encoder reranking by default. Default: true. */\n rerank: boolean;\n /** Recency boost half-life in days. 0 = off. Default: 90. */\n recencyBoostDays: number;\n /** Default max results. Default: 10. */\n defaultLimit: number;\n /** Default snippet length for MCP results. Default: 200. */\n snippetLength: number;\n}\n\nexport interface PostgresConfig {\n /** Connection string — if set, overrides individual host/port/etc. fields */\n connectionString?: string;\n /** Postgres host (default: \"localhost\") */\n host?: string;\n /** Postgres port (default: 5432) */\n port?: number;\n /** Postgres database name (default: \"pai\") */\n database?: string;\n /** Postgres user (default: \"pai\") */\n user?: string;\n /** Postgres password (default: \"pai\") */\n password?: string;\n /** Maximum pool connections (default: 5) */\n maxConnections?: number;\n /** Connection timeout in ms (default: 5000) */\n connectionTimeoutMs?: number;\n}\n\nexport interface PaiDaemonConfig {\n /** Unix Domain Socket path for IPC */\n socketPath: string;\n\n /** Index schedule interval in seconds (default: 300 = 5 minutes) */\n indexIntervalSecs: number;\n\n /** Embedding schedule interval in seconds (default: 600 = 10 minutes) */\n embedIntervalSecs: number;\n\n /** Storage backend: \"sqlite\" (default) or \"postgres\" */\n storageBackend: \"sqlite\" | \"postgres\";\n\n /** PostgreSQL connection config (used when storageBackend = \"postgres\") */\n postgres?: PostgresConfig;\n\n /** Embedding model name (used for semantic/hybrid search) */\n embeddingModel: string;\n\n /** Log level */\n logLevel: \"debug\" | \"info\" | \"warn\" | \"error\";\n\n /** Obsidian vault root path for zettelkasten indexing. If set, vault indexing runs alongside project indexing. */\n vaultPath?: string;\n\n /** Registry project_id to use for vault chunks in memory_chunks. Default: auto-detected. */\n vaultProjectId?: number;\n\n /** Notification subsystem configuration */\n notifications: NotificationConfig;\n\n /** Search defaults — applied when MCP tool or CLI doesn't specify a value */\n search: SearchConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Per-user Postgres isolation\n// ---------------------------------------------------------------------------\n\n/** Derive a per-user Postgres database name: pai_<username> */\nfunction perUserDbName(): string {\n const username = userInfo().username;\n // Sanitize: only allow alphanumeric and underscore for Postgres identifiers\n const safe = username.replace(/[^a-zA-Z0-9_]/g, \"_\").toLowerCase();\n return `pai_${safe}`;\n}\n\n/** Derive the per-user connection string */\nfunction perUserConnectionString(): string {\n const db = perUserDbName();\n return `postgresql://pai:pai@localhost:5432/${db}`;\n}\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\n\nexport const DEFAULTS: PaiDaemonConfig = {\n socketPath: \"/tmp/pai.sock\",\n indexIntervalSecs: 300,\n embedIntervalSecs: 600,\n storageBackend: \"sqlite\",\n postgres: {\n connectionString: perUserConnectionString(),\n maxConnections: 5,\n connectionTimeoutMs: 5000,\n },\n embeddingModel: \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n logLevel: \"info\",\n notifications: DEFAULT_NOTIFICATION_CONFIG,\n search: {\n mode: \"keyword\",\n rerank: true,\n recencyBoostDays: 90,\n defaultLimit: 10,\n snippetLength: 200,\n },\n};\n\n/** Config template — generated at runtime so the DB name is per-user */\nfunction configTemplate(): string {\n return `{\n \"socketPath\": \"/tmp/pai.sock\",\n \"indexIntervalSecs\": 300,\n \"embedIntervalSecs\": 600,\n \"storageBackend\": \"sqlite\",\n \"postgres\": {\n \"connectionString\": \"${perUserConnectionString()}\",\n \"maxConnections\": 5,\n \"connectionTimeoutMs\": 5000\n },\n \"embeddingModel\": \"Snowflake/snowflake-arctic-embed-m-v1.5\",\n \"logLevel\": \"info\",\n \"vaultPath\": \"\",\n \"vaultProjectId\": 0,\n \"search\": {\n \"mode\": \"keyword\",\n \"rerank\": true,\n \"recencyBoostDays\": 90,\n \"defaultLimit\": 10,\n \"snippetLength\": 200\n }\n}\n`;\n}\n\n// ---------------------------------------------------------------------------\n// Path helpers\n// ---------------------------------------------------------------------------\n\n/** Expand a leading ~ to the real home directory */\nexport function expandHome(p: string): string {\n if (p === \"~\" || p.startsWith(\"~/\") || p.startsWith(\"~\\\\\")) {\n return join(homedir(), p.slice(1));\n }\n return p;\n}\n\nexport const CONFIG_DIR = join(homedir(), \".config\", \"pai\");\nexport const CONFIG_FILE = join(CONFIG_DIR, \"config.json\");\n\n// ---------------------------------------------------------------------------\n// Deep merge (handles nested objects, not arrays)\n// ---------------------------------------------------------------------------\n\nfunction deepMerge<T extends object>(\n target: T,\n source: Record<string, unknown>\n): T {\n const result = { ...target };\n for (const key of Object.keys(source)) {\n const srcVal = source[key];\n if (srcVal === undefined || srcVal === null) continue;\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n typeof srcVal === \"object\" &&\n !Array.isArray(srcVal) &&\n typeof tgtVal === \"object\" &&\n tgtVal !== null &&\n !Array.isArray(tgtVal)\n ) {\n (result as Record<string, unknown>)[key] = deepMerge(\n tgtVal as object,\n srcVal as Record<string, unknown>\n );\n } else {\n (result as Record<string, unknown>)[key] = srcVal;\n }\n }\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Config loader\n// ---------------------------------------------------------------------------\n\n/**\n * Load configuration from ~/.config/pai/config.json.\n * Returns defaults merged with any values found in the file.\n */\nexport function loadConfig(): PaiDaemonConfig {\n if (!existsSync(CONFIG_FILE)) {\n return { ...DEFAULTS };\n }\n\n let raw: string;\n try {\n raw = readFileSync(CONFIG_FILE, \"utf-8\");\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not read config file at ${CONFIG_FILE}: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(raw) as Record<string, unknown>;\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Config file is not valid JSON: ${e}\\n`\n );\n return { ...DEFAULTS };\n }\n\n // Compat: config.json may use \"obsidianVaultPath\" (legacy key) instead of \"vaultPath\".\n // Map it across so the daemon picks it up correctly.\n if (parsed.obsidianVaultPath && !parsed.vaultPath) {\n parsed.vaultPath = parsed.obsidianVaultPath;\n process.stderr.write(\n `[pai-daemon] Config: mapped obsidianVaultPath → vaultPath (${parsed.vaultPath})\\n`\n );\n }\n\n return deepMerge(DEFAULTS, parsed);\n}\n\n/**\n * Ensure ~/.config/pai/ exists and write a default config.json template\n * if none exists yet. Call this only from the `serve` command.\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n process.stderr.write(\n `[pai-daemon] Created config directory: ${CONFIG_DIR}\\n`\n );\n }\n\n if (!existsSync(CONFIG_FILE)) {\n try {\n writeFileSync(CONFIG_FILE, configTemplate(), \"utf-8\");\n process.stderr.write(\n `[pai-daemon] Wrote default config to: ${CONFIG_FILE}\\n`\n );\n } catch (e) {\n process.stderr.write(\n `[pai-daemon] Could not write default config: ${e}\\n`\n );\n }\n }\n}\n"],"mappings":"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"}