@tekmidian/pai 0.3.2 → 0.5.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/ARCHITECTURE.md +16 -10
- package/README.md +46 -6
- package/dist/{auto-route-JjW3f7pV.mjs → auto-route-B5MSUJZK.mjs} +3 -3
- package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-B5MSUJZK.mjs.map} +1 -1
- package/dist/cli/index.mjs +313 -43
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{config-DELNqq3Z.mjs → config-B4brrHHE.mjs} +1 -1
- package/dist/{config-DELNqq3Z.mjs.map → config-B4brrHHE.mjs.map} +1 -1
- package/dist/daemon/index.mjs +7 -7
- package/dist/daemon-mcp/index.mjs +11 -4
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{daemon-CeTX4NpF.mjs → daemon-s868Paua.mjs} +12 -12
- package/dist/{daemon-CeTX4NpF.mjs.map → daemon-s868Paua.mjs.map} +1 -1
- package/dist/{detect-D7gPV3fQ.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-D7gPV3fQ.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-cYYhK2Mi.mjs → detector-Bp-2SM3x.mjs} +2 -2
- package/dist/{detector-cYYhK2Mi.mjs.map → detector-Bp-2SM3x.mjs.map} +1 -1
- package/dist/{factory-DZLvRf4m.mjs → factory-CeXQzlwn.mjs} +3 -3
- package/dist/{factory-DZLvRf4m.mjs.map → factory-CeXQzlwn.mjs.map} +1 -1
- package/dist/hooks/capture-all-events.mjs +238 -0
- package/dist/hooks/capture-all-events.mjs.map +7 -0
- package/dist/hooks/capture-session-summary.mjs +198 -0
- package/dist/hooks/capture-session-summary.mjs.map +7 -0
- package/dist/hooks/capture-tool-output.mjs +105 -0
- package/dist/hooks/capture-tool-output.mjs.map +7 -0
- package/dist/hooks/cleanup-session-files.mjs +129 -0
- package/dist/hooks/cleanup-session-files.mjs.map +7 -0
- package/dist/hooks/context-compression-hook.mjs +283 -0
- package/dist/hooks/context-compression-hook.mjs.map +7 -0
- package/dist/hooks/initialize-session.mjs +206 -0
- package/dist/hooks/initialize-session.mjs.map +7 -0
- package/dist/hooks/load-core-context.mjs +110 -0
- package/dist/hooks/load-core-context.mjs.map +7 -0
- package/dist/hooks/load-project-context.mjs +548 -0
- package/dist/hooks/load-project-context.mjs.map +7 -0
- package/dist/hooks/security-validator.mjs +159 -0
- package/dist/hooks/security-validator.mjs.map +7 -0
- package/dist/hooks/stop-hook.mjs +625 -0
- package/dist/hooks/stop-hook.mjs.map +7 -0
- package/dist/hooks/subagent-stop-hook.mjs +152 -0
- package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
- package/dist/hooks/sync-todo-to-md.mjs +322 -0
- package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
- package/dist/hooks/update-tab-on-action.mjs +90 -0
- package/dist/hooks/update-tab-on-action.mjs.map +7 -0
- package/dist/hooks/update-tab-titles.mjs +55 -0
- package/dist/hooks/update-tab-titles.mjs.map +7 -0
- package/dist/index.d.mts +29 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -3
- package/dist/{indexer-backend-BHztlJJg.mjs → indexer-backend-DQO-FqAI.mjs} +1 -1
- package/dist/{indexer-backend-BHztlJJg.mjs.map → indexer-backend-DQO-FqAI.mjs.map} +1 -1
- package/dist/{ipc-client-CLt2fNlC.mjs → ipc-client-CgSpwHDC.mjs} +1 -1
- package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-CgSpwHDC.mjs.map} +1 -1
- package/dist/mcp/index.mjs +15 -5
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{postgres-CRBe30Ag.mjs → postgres-CIxeqf_n.mjs} +1 -1
- package/dist/{postgres-CRBe30Ag.mjs.map → postgres-CIxeqf_n.mjs.map} +1 -1
- package/dist/reranker-D7bRAHi6.mjs +71 -0
- package/dist/reranker-D7bRAHi6.mjs.map +1 -0
- package/dist/{schemas-BY3Pjvje.mjs → schemas-BFIgGntb.mjs} +1 -1
- package/dist/{schemas-BY3Pjvje.mjs.map → schemas-BFIgGntb.mjs.map} +1 -1
- package/dist/{search-GK0ibTJy.mjs → search-_oHfguA5.mjs} +47 -4
- package/dist/search-_oHfguA5.mjs.map +1 -0
- package/dist/{sqlite-RyR8Up1v.mjs → sqlite-CymLKiDE.mjs} +2 -2
- package/dist/{sqlite-RyR8Up1v.mjs.map → sqlite-CymLKiDE.mjs.map} +1 -1
- package/dist/{tools-CUg0Lyg-.mjs → tools-Dx7GjOHd.mjs} +23 -14
- package/dist/tools-Dx7GjOHd.mjs.map +1 -0
- package/dist/{vault-indexer-Bo2aPSzP.mjs → vault-indexer-DXWs9pDn.mjs} +1 -1
- package/dist/{vault-indexer-Bo2aPSzP.mjs.map → vault-indexer-DXWs9pDn.mjs.map} +1 -1
- package/dist/{zettelkasten-Co-w0XSZ.mjs → zettelkasten-e-a4rW_6.mjs} +2 -2
- package/dist/{zettelkasten-Co-w0XSZ.mjs.map → zettelkasten-e-a4rW_6.mjs.map} +1 -1
- package/package.json +4 -2
- package/scripts/build-hooks.mjs +51 -0
- package/src/hooks/ts/capture-all-events.ts +179 -0
- package/src/hooks/ts/lib/detect-environment.ts +53 -0
- package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
- package/src/hooks/ts/lib/pai-paths.ts +124 -0
- package/src/hooks/ts/lib/project-utils.ts +914 -0
- package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
- package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
- package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
- package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
- package/src/hooks/ts/session-start/initialize-session.ts +155 -0
- package/src/hooks/ts/session-start/load-core-context.ts +104 -0
- package/src/hooks/ts/session-start/load-project-context.ts +394 -0
- package/src/hooks/ts/stop/stop-hook.ts +407 -0
- package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
- package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
- package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
- package/tab-color-command.sh +24 -0
- package/templates/skills/createskill-skill.template.md +78 -0
- package/templates/skills/history-system.template.md +371 -0
- package/templates/skills/hook-system.template.md +913 -0
- package/templates/skills/sessions-skill.template.md +102 -0
- package/templates/skills/skill-system.template.md +214 -0
- package/templates/skills/terminal-tabs.template.md +120 -0
- package/dist/search-GK0ibTJy.mjs.map +0 -1
- package/dist/tools-CUg0Lyg-.mjs.map +0 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!-- Generated by PAI Setup -->
|
|
2
|
+
---
|
|
3
|
+
name: Sessions
|
|
4
|
+
description: Navigate and manage PAI sessions across all projects. USE WHEN user says "list sessions", "where was I working", "show my sessions", "find session", "continue work from", "switch to project", "open project", "name project", "group projects", "consolidate", "organize projects", "clean up sessions", "work on X", "start working on X", "show me what we did on X", "bring notes together", OR asks about past sessions, previous work, or project navigation.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## How Session Notes Work
|
|
8
|
+
|
|
9
|
+
Session notes track what was done in each Claude session. They live in a `Notes/` directory.
|
|
10
|
+
|
|
11
|
+
**The key concept: notes route to a project.** When the user says "work on PAI" or "start working on myproject", you set the routing so all session notes go to that project's Notes directory. The routing is stored in `~/.claude/session-routing.json`.
|
|
12
|
+
|
|
13
|
+
**Where notes actually live:**
|
|
14
|
+
- Each project has a Notes directory (usually in an Obsidian vault or local Notes folder)
|
|
15
|
+
- Code repos can symlink their `Notes/` to a vault path
|
|
16
|
+
- Claude also keeps internal copies in `~/.claude/projects/*/Notes/` (for search/history)
|
|
17
|
+
- The `pai` CLI manages everything
|
|
18
|
+
|
|
19
|
+
**Project paths** are stored in `~/.claude/session-routing.json` under `project_paths`. Add your own projects by editing that file.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## What the User Might Say → What You Do
|
|
24
|
+
|
|
25
|
+
**"Work on PAI" / "Start working on myproject" / "Let's do PAI stuff"**
|
|
26
|
+
→ Run `pai route <project-name>`. Tell user: "Notes now routing to [path]."
|
|
27
|
+
|
|
28
|
+
**"Show me the sessions where we worked on PAI" / "What did we do on myproject?"**
|
|
29
|
+
→ Run `pai search <project-name>`. Show the matching sessions with dates and summaries.
|
|
30
|
+
|
|
31
|
+
**"Bring the PAI notes together" / "Consolidate PAI sessions"**
|
|
32
|
+
→ Search for all sessions matching the project name across all projects. Read the session notes. Combine them into a coherent summary. Write a consolidated note if asked.
|
|
33
|
+
|
|
34
|
+
**"List sessions" / "Show recent sessions" / "/sessions"**
|
|
35
|
+
→ ALWAYS run `pai session active` FIRST to show currently open Claude Code tabs.
|
|
36
|
+
→ Then run `pai project list` and `pai session list` for the full overview.
|
|
37
|
+
→ Present active sessions PROMINENTLY at the top of your response.
|
|
38
|
+
|
|
39
|
+
**"Where are my notes going?" / "What's the current routing?"**
|
|
40
|
+
→ Run `pai route` (no args) to show current status.
|
|
41
|
+
|
|
42
|
+
**"Stop routing" / "Clear routing" / "Back to default"**
|
|
43
|
+
→ Run `pai route clear`.
|
|
44
|
+
|
|
45
|
+
**"Open the myproject project"**
|
|
46
|
+
→ Run `pai open <project-name> --claude` to open in a new terminal tab.
|
|
47
|
+
|
|
48
|
+
**"Continue work from the myproject project"**
|
|
49
|
+
→ Search for relevant sessions, read the notes and TODO.md, present context summary, continue working here.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Commands Reference
|
|
54
|
+
|
|
55
|
+
| Command | What it does |
|
|
56
|
+
|---------|-------------|
|
|
57
|
+
| `pai session active` | **Show currently open Claude Code sessions** (checks JSONL timestamps) |
|
|
58
|
+
| `pai session active --minutes 120` | Widen the detection window (default: 60 min) |
|
|
59
|
+
| `pai session active --json` | JSON output for programmatic use |
|
|
60
|
+
| `pai` | Show all projects with session counts |
|
|
61
|
+
| `pai session list` | Detailed session list |
|
|
62
|
+
| `pai search "keyword"` | Find sessions by keyword |
|
|
63
|
+
| `pai route <project>` | Route notes to a project |
|
|
64
|
+
| `pai route` | Show current routing status |
|
|
65
|
+
| `pai route clear` | Stop routing, use defaults |
|
|
66
|
+
| `pai open <project> --claude` | Open project in new tab |
|
|
67
|
+
| `pai name "Name"` | Name the current project |
|
|
68
|
+
| `pai info <number>` | Deep dive into a project |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
**Example 1: "Show me what we did on PAI"**
|
|
75
|
+
```
|
|
76
|
+
→ Run: pai search pai
|
|
77
|
+
→ Show matching sessions with dates
|
|
78
|
+
→ Offer to read specific session notes for details
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Example 2: "Let's work on myproject"**
|
|
82
|
+
```
|
|
83
|
+
→ Run: pai route myproject
|
|
84
|
+
→ Output: "Notes now routing to .../myproject/Notes"
|
|
85
|
+
→ Read myproject's TODO.md to see pending work
|
|
86
|
+
→ Continue from where we left off
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Example 3: "Bring all the PAI sessions together"**
|
|
90
|
+
```
|
|
91
|
+
→ Run: pai search pai (find all PAI-related sessions)
|
|
92
|
+
→ Read each session note
|
|
93
|
+
→ Create a consolidated summary
|
|
94
|
+
→ Optionally write it to PAI's Notes directory
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Example 4: "Where was I working on the appstore?"**
|
|
98
|
+
```
|
|
99
|
+
→ Run: pai search appstore
|
|
100
|
+
→ Show matching projects with session counts
|
|
101
|
+
→ Offer to open or read details
|
|
102
|
+
```
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<!-- Generated by PAI Setup -->
|
|
2
|
+
# Custom Skill System
|
|
3
|
+
|
|
4
|
+
**The MANDATORY configuration system for ALL PAI skills.**
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## THIS IS THE AUTHORITATIVE SOURCE
|
|
9
|
+
|
|
10
|
+
This document defines the **required structure** for every skill in the PAI system.
|
|
11
|
+
|
|
12
|
+
**ALL skill creation MUST follow this structure.**
|
|
13
|
+
|
|
14
|
+
**"Canonicalize a skill"** = Restructure it to match this exact format, including TitleCase naming.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## TitleCase Naming Convention (MANDATORY)
|
|
19
|
+
|
|
20
|
+
**All naming in the skill system MUST use TitleCase (PascalCase).**
|
|
21
|
+
|
|
22
|
+
| Component | Wrong | Correct |
|
|
23
|
+
|-----------|-------|---------|
|
|
24
|
+
| Skill directory | `createskill`, `create-skill` | `Createskill` |
|
|
25
|
+
| Workflow files | `create.md`, `update-info.md` | `Create.md`, `UpdateInfo.md` |
|
|
26
|
+
| Reference docs | `prosody-guide.md` | `ProsodyGuide.md` |
|
|
27
|
+
| Tool files | `manage-server.ts` | `ManageServer.ts` |
|
|
28
|
+
| YAML name | `name: create-skill` | `name: Createskill` |
|
|
29
|
+
|
|
30
|
+
**Exception:** `SKILL.md` is always uppercase (convention for the main skill file).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## The Required Structure
|
|
35
|
+
|
|
36
|
+
Every SKILL.md has two parts:
|
|
37
|
+
|
|
38
|
+
### 1. YAML Frontmatter (Single-Line Description)
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
---
|
|
42
|
+
name: SkillName
|
|
43
|
+
description: [What it does]. USE WHEN [intent triggers using OR]. [Additional capabilities].
|
|
44
|
+
---
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Rules:**
|
|
48
|
+
- `name` uses **TitleCase**
|
|
49
|
+
- `description` is a **single line** (not multi-line with `|`)
|
|
50
|
+
- `USE WHEN` keyword is **MANDATORY** (Claude Code parses this for skill activation)
|
|
51
|
+
- Use intent-based triggers with `OR` for multiple conditions
|
|
52
|
+
- Max 1024 characters (Anthropic hard limit)
|
|
53
|
+
|
|
54
|
+
### 2. Markdown Body
|
|
55
|
+
|
|
56
|
+
```markdown
|
|
57
|
+
# SkillName
|
|
58
|
+
|
|
59
|
+
[Brief description]
|
|
60
|
+
|
|
61
|
+
## Workflow Routing
|
|
62
|
+
|
|
63
|
+
**When executing a workflow, do BOTH of these:**
|
|
64
|
+
|
|
65
|
+
1. **Call the notification script** (for observability tracking):
|
|
66
|
+
```bash
|
|
67
|
+
~/.claude/Tools/SkillWorkflowNotification WORKFLOWNAME SKILLNAME
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. **Output the text notification** (for user visibility):
|
|
71
|
+
```
|
|
72
|
+
Running the **WorkflowName** workflow from the **SKILLNAME** skill...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This ensures workflows appear in the observability dashboard AND the user sees the announcement.
|
|
76
|
+
|
|
77
|
+
| Workflow | Trigger | File |
|
|
78
|
+
|----------|---------|------|
|
|
79
|
+
| **WorkflowOne** | "trigger phrase" | `workflows/WorkflowOne.md` |
|
|
80
|
+
| **WorkflowTwo** | "another trigger" | `workflows/WorkflowTwo.md` |
|
|
81
|
+
|
|
82
|
+
## Examples
|
|
83
|
+
|
|
84
|
+
**Example 1: [Common use case]**
|
|
85
|
+
```
|
|
86
|
+
User: "[Typical user request]"
|
|
87
|
+
→ Invokes WorkflowOne workflow
|
|
88
|
+
→ [What skill does]
|
|
89
|
+
→ [What user gets back]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## [Additional Sections]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Examples Section (REQUIRED)
|
|
98
|
+
|
|
99
|
+
**Every skill MUST have an `## Examples` section** showing 2-3 concrete usage patterns.
|
|
100
|
+
|
|
101
|
+
**Why Examples Matter:**
|
|
102
|
+
- Anthropic research shows examples improve tool selection accuracy from 72% to 90%
|
|
103
|
+
- Descriptions tell Claude WHEN to activate; examples show HOW the skill works
|
|
104
|
+
|
|
105
|
+
**Example Format:**
|
|
106
|
+
```markdown
|
|
107
|
+
## Examples
|
|
108
|
+
|
|
109
|
+
**Example 1: [Use case name]**
|
|
110
|
+
```
|
|
111
|
+
User: "[Actual user request]"
|
|
112
|
+
→ Invokes WorkflowName workflow
|
|
113
|
+
→ [What the skill does]
|
|
114
|
+
→ [What user receives back]
|
|
115
|
+
```
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Intent Matching, Not String Matching
|
|
121
|
+
|
|
122
|
+
We use **intent matching**, not exact phrase matching.
|
|
123
|
+
|
|
124
|
+
**Example description:**
|
|
125
|
+
```yaml
|
|
126
|
+
description: Complete blog workflow. USE WHEN user mentions doing anything with their blog, website, site, including things like update, proofread, write, edit, publish, preview, blog posts, or website pages.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Key Principles:**
|
|
130
|
+
- Use intent language: "user mentions", "user wants to", "including things like"
|
|
131
|
+
- Don't list exact phrases in quotes
|
|
132
|
+
- Cover the domain conceptually
|
|
133
|
+
- Use `OR` to combine multiple trigger conditions
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Directory Structure
|
|
138
|
+
|
|
139
|
+
Every skill follows this structure:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
SkillName/ # TitleCase directory name
|
|
143
|
+
├── SKILL.md # Main skill file (always uppercase)
|
|
144
|
+
├── ReferenceDoc.md # Optional: Reference docs (TitleCase)
|
|
145
|
+
├── tools/ # CLI tools (ALWAYS present, even if empty)
|
|
146
|
+
│ ├── ToolName.ts # TypeScript CLI tool (TitleCase)
|
|
147
|
+
│ └── ToolName.help.md # Tool documentation (TitleCase)
|
|
148
|
+
└── workflows/
|
|
149
|
+
├── Create.md # Work execution workflow (TitleCase)
|
|
150
|
+
└── Update.md # Work execution workflow (TitleCase)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Workflows vs Reference Documentation
|
|
156
|
+
|
|
157
|
+
**CRITICAL DISTINCTION:**
|
|
158
|
+
|
|
159
|
+
### Workflows (`workflows/` directory)
|
|
160
|
+
- Operational procedures (create, update, delete, deploy)
|
|
161
|
+
- Step-by-step execution instructions
|
|
162
|
+
- Actions that change state or produce output
|
|
163
|
+
- Things you "run" or "execute"
|
|
164
|
+
|
|
165
|
+
### Reference Documentation (skill root)
|
|
166
|
+
- Guides and how-to documentation
|
|
167
|
+
- Specifications and schemas
|
|
168
|
+
- Information you "read" or "reference"
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Complete Checklist
|
|
173
|
+
|
|
174
|
+
Before a skill is complete:
|
|
175
|
+
|
|
176
|
+
### Naming (TitleCase)
|
|
177
|
+
- [ ] Skill directory uses TitleCase
|
|
178
|
+
- [ ] All workflow files use TitleCase
|
|
179
|
+
- [ ] All reference docs use TitleCase
|
|
180
|
+
- [ ] YAML `name:` uses TitleCase
|
|
181
|
+
|
|
182
|
+
### YAML Frontmatter
|
|
183
|
+
- [ ] Single-line description with embedded `USE WHEN` clause
|
|
184
|
+
- [ ] No separate `triggers:` or `workflows:` arrays
|
|
185
|
+
- [ ] Description under 1024 characters
|
|
186
|
+
|
|
187
|
+
### Markdown Body
|
|
188
|
+
- [ ] `## Workflow Routing` section with table format
|
|
189
|
+
- [ ] `## Examples` section with 2-3 concrete patterns
|
|
190
|
+
- [ ] All workflows have routing entries
|
|
191
|
+
|
|
192
|
+
### Structure
|
|
193
|
+
- [ ] `tools/` directory exists (even if empty)
|
|
194
|
+
- [ ] No `backups/` directory inside skill
|
|
195
|
+
- [ ] Workflows contain ONLY execution procedures
|
|
196
|
+
- [ ] Reference docs live at skill root
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Summary
|
|
201
|
+
|
|
202
|
+
| Component | Purpose | Naming |
|
|
203
|
+
|-----------|---------|--------|
|
|
204
|
+
| **Skill directory** | Contains all skill files | TitleCase (e.g., `Blogging`) |
|
|
205
|
+
| **SKILL.md** | Main skill file | Always uppercase |
|
|
206
|
+
| **Workflow files** | Execution procedures | TitleCase (e.g., `Create.md`) |
|
|
207
|
+
| **Reference docs** | Information to read | TitleCase (e.g., `ApiReference.md`) |
|
|
208
|
+
| **Tool files** | CLI automation | TitleCase (e.g., `ManageServer.ts`) |
|
|
209
|
+
|
|
210
|
+
This system ensures:
|
|
211
|
+
1. Skills invoke properly based on intent (USE WHEN in description)
|
|
212
|
+
2. Specific functionality executes accurately (Workflow Routing in body)
|
|
213
|
+
3. All skills have consistent, predictable structure
|
|
214
|
+
4. **All naming follows TitleCase convention**
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!-- Generated by PAI Setup -->
|
|
2
|
+
# Terminal Tab Title System
|
|
3
|
+
|
|
4
|
+
## Overview
|
|
5
|
+
|
|
6
|
+
The PAI system automatically updates your terminal tab title with a 4-word summary of what was done after each task completion. This provides instant visual feedback in your terminal tabs, making it easy to see what each Claude session accomplished.
|
|
7
|
+
|
|
8
|
+
## How It Works
|
|
9
|
+
|
|
10
|
+
The `stop-hook.ts` hook runs after every task completion and:
|
|
11
|
+
|
|
12
|
+
1. **Extracts the task summary** from the COMPLETED line in responses
|
|
13
|
+
2. **Generates a 4-word title** that summarizes what was accomplished
|
|
14
|
+
3. **Updates your terminal tab** using ANSI escape sequences
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
### 4-Word Summary Format
|
|
19
|
+
|
|
20
|
+
The system creates meaningful 4-word summaries by:
|
|
21
|
+
- Using past-tense action verbs (Created, Updated, Fixed, etc.)
|
|
22
|
+
- Extracting key nouns from the task
|
|
23
|
+
- Prioritizing words from the COMPLETED line when available
|
|
24
|
+
- Falling back to the user's original query if needed
|
|
25
|
+
|
|
26
|
+
### Examples
|
|
27
|
+
|
|
28
|
+
| User Query | Tab Title |
|
|
29
|
+
|------------|-----------|
|
|
30
|
+
| "Update the README documentation" | Updated Readme Documentation Done |
|
|
31
|
+
| "Fix the stop-hook" | Fixed Stop Hook Successfully |
|
|
32
|
+
| "Research AI trends" | Researched AI Trends Complete |
|
|
33
|
+
| "Implement new feature" | Implemented New Feature Done |
|
|
34
|
+
|
|
35
|
+
## Terminal Compatibility
|
|
36
|
+
|
|
37
|
+
The tab title system works with terminals that support OSC (Operating System Command) sequences:
|
|
38
|
+
|
|
39
|
+
- **Kitty** - Full support
|
|
40
|
+
- **iTerm2** - Full support
|
|
41
|
+
- **Terminal.app** - Full support
|
|
42
|
+
- **Alacritty** - Full support
|
|
43
|
+
- **VS Code Terminal** - Full support
|
|
44
|
+
|
|
45
|
+
## Implementation Details
|
|
46
|
+
|
|
47
|
+
### Escape Sequences Used
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# OSC 0 - Sets icon and window title
|
|
51
|
+
printf '\033]0;Title Here\007'
|
|
52
|
+
|
|
53
|
+
# OSC 2 - Sets window title
|
|
54
|
+
printf '\033]2;Title Here\007'
|
|
55
|
+
|
|
56
|
+
# OSC 30 - Kitty-specific tab title
|
|
57
|
+
printf '\033]30;Title Here\007'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Hook Location
|
|
61
|
+
|
|
62
|
+
The terminal tab functionality is implemented in:
|
|
63
|
+
```
|
|
64
|
+
${PAI_DIR}/Hooks/stop-hook.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Key Functions
|
|
68
|
+
|
|
69
|
+
1. **generateTabTitle(prompt, completedLine)** - Creates the 4-word summary
|
|
70
|
+
2. **setTabTitle(title)** - Sends escape sequences to update the tab
|
|
71
|
+
3. **Hook execution** - Runs automatically after every task
|
|
72
|
+
|
|
73
|
+
## Debugging
|
|
74
|
+
|
|
75
|
+
If tab titles aren't updating:
|
|
76
|
+
|
|
77
|
+
1. **Check hook is executable:**
|
|
78
|
+
```bash
|
|
79
|
+
ls -la ${PAI_DIR}/Hooks/stop-hook.ts
|
|
80
|
+
# Should show: -rwxr-xr-x
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **Verify Claude Code settings:**
|
|
84
|
+
- Ensure stop-hook is configured in your Claude Code settings
|
|
85
|
+
- Path should be: `${PAI_DIR}/Hooks/stop-hook.ts`
|
|
86
|
+
|
|
87
|
+
3. **Test manually:**
|
|
88
|
+
```bash
|
|
89
|
+
printf '\033]0;Test Title\007' >&2
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
4. **Check stderr output:**
|
|
93
|
+
The hook logs to stderr with:
|
|
94
|
+
- Tab title changes
|
|
95
|
+
- User queries processed
|
|
96
|
+
- Completed text extracted
|
|
97
|
+
|
|
98
|
+
## Customization
|
|
99
|
+
|
|
100
|
+
To modify the tab title behavior, edit `${PAI_DIR}/Hooks/stop-hook.ts`:
|
|
101
|
+
|
|
102
|
+
- Change word count (currently 4 words)
|
|
103
|
+
- Modify verb tense (currently past tense)
|
|
104
|
+
- Add custom prefixes or suffixes
|
|
105
|
+
- Filter different stop words
|
|
106
|
+
|
|
107
|
+
## Benefits
|
|
108
|
+
|
|
109
|
+
- **Visual Task Tracking** - See what each tab accomplished at a glance
|
|
110
|
+
- **Multi-Session Management** - Easily identify different Claude sessions
|
|
111
|
+
- **Task History** - Tab titles persist as a record of completed work
|
|
112
|
+
- **No Manual Updates** - Fully automatic, runs on every task completion
|
|
113
|
+
|
|
114
|
+
## Integration with Voice System
|
|
115
|
+
|
|
116
|
+
The terminal tab system works alongside the voice notification system:
|
|
117
|
+
- Both extract information from the COMPLETED line
|
|
118
|
+
- Tab gets a 4-word visual summary
|
|
119
|
+
- Voice speaks the completion message
|
|
120
|
+
- Both provide immediate feedback through different channels
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-GK0ibTJy.mjs","names":[],"sources":["../src/memory/search.ts"],"sourcesContent":["/**\n * Search over the PAI federation memory index.\n *\n * Provides three search modes:\n * - keyword — BM25 full-text search (default, fast, no ML required)\n * - semantic — Brute-force cosine similarity over pre-computed embeddings\n * - hybrid — Normalized combination of BM25 + cosine scores\n *\n * BM25 uses SQLite's FTS5 extension. Semantic search requires embeddings to\n * have been generated first via `embedChunks()` in the indexer.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport { deserializeEmbedding, cosineSimilarity } from \"./embeddings.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SearchResult {\n projectId: number;\n projectSlug?: string; // populated from registry after search when available\n path: string;\n startLine: number;\n endLine: number;\n snippet: string;\n score: number; // raw BM25 score (lower = more relevant in FTS5)\n tier: string;\n source: string;\n}\n\nexport interface SearchOptions {\n /** Restrict search to these project IDs. */\n projectIds?: number[];\n /** Restrict to 'memory' or 'notes' sources. */\n sources?: string[];\n /** Restrict to specific tier(s): 'evergreen' | 'daily' | 'topic' | 'session' */\n tiers?: string[];\n /** Maximum number of results to return. Default 10. */\n maxResults?: number;\n /** Minimum BM25 score threshold (FTS5 scores are negative; 0.0 means no filter). */\n minScore?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Stop words\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"a\", \"an\", \"and\", \"are\", \"as\", \"at\", \"be\", \"been\", \"but\", \"by\",\n \"do\", \"for\", \"from\", \"has\", \"have\", \"he\", \"her\", \"him\", \"his\",\n \"how\", \"i\", \"if\", \"in\", \"is\", \"it\", \"its\", \"me\", \"my\", \"not\",\n \"of\", \"on\", \"or\", \"our\", \"out\", \"she\", \"so\", \"that\", \"the\",\n \"their\", \"them\", \"they\", \"this\", \"to\", \"up\", \"us\", \"was\", \"we\",\n \"were\", \"what\", \"when\", \"who\", \"will\", \"with\", \"you\", \"your\",\n]);\n\n// ---------------------------------------------------------------------------\n// Query builder\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a free-text query into an FTS5 query string.\n *\n * Strategy:\n * 1. Tokenise by whitespace and punctuation\n * 2. Remove stop words and tokens shorter than 2 characters\n * 3. Double-quote each remaining token (exact word form)\n * 4. Join with OR so that any matching token returns a result\n *\n * Using OR instead of AND is critical for multi-word queries: the words rarely\n * all appear in the same chunk, so AND would return zero results. FTS5 BM25\n * scoring naturally ranks chunks where more terms match higher, so the most\n * relevant chunks still surface at the top.\n *\n * Example: \"Synchrotech interview follow-up Gilles\"\n * → `\"synchrotech\" OR \"interview\" OR \"follow\" OR \"gilles\"`\n * → chunks matching any term, ranked by how many terms match\n */\nexport function buildFtsQuery(query: string): string {\n const tokens = query\n .toLowerCase()\n .split(/[\\s\\p{P}]+/u)\n .filter(Boolean)\n .filter((t) => t.length >= 2)\n .filter((t) => !STOP_WORDS.has(t))\n // Escape any double-quotes inside the token (FTS5 uses them as delimiters)\n .map((t) => `\"${t.replace(/\"/g, '\"\"')}\"`)\n\n if (tokens.length === 0) {\n // Fallback: use original query as a raw string (may produce no results)\n return `\"${query.replace(/\"/g, '\"\"')}\"`;\n }\n\n return tokens.join(\" OR \");\n}\n\n// ---------------------------------------------------------------------------\n// Search\n// ---------------------------------------------------------------------------\n\n/**\n * Search across all indexed memory using FTS5 BM25 ranking.\n *\n * Results are ordered by BM25 score (most relevant first).\n * FTS5 bm25() returns negative values; closer to 0 = more relevant.\n * We negate the score so callers get positive values where higher = better.\n *\n * Multilingual note: SQLite FTS5 uses the `unicode61` tokenizer by default,\n * which handles Unicode correctly (German umlauts, French accents, etc.) without\n * language-specific stemming. No changes needed here — it is already\n * multilingual-safe.\n */\nexport function searchMemory(\n db: Database,\n query: string,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const ftsQuery = buildFtsQuery(query);\n\n // Build the SQL with optional filters\n const conditions: string[] = [];\n const params: (string | number)[] = [ftsQuery];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`c.project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`c.source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`c.tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const whereClause = conditions.length > 0\n ? \"AND \" + conditions.join(\" AND \")\n : \"\";\n\n params.push(maxResults);\n\n // FTS5: join memory_fts with memory_chunks to get metadata\n // bm25(memory_fts) returns negative values (lower = better match)\n const sql = `\n SELECT\n c.project_id,\n c.path,\n c.start_line,\n c.end_line,\n c.text AS snippet,\n c.tier,\n c.source,\n bm25(memory_fts) AS bm25_score\n FROM memory_fts\n JOIN memory_chunks c ON memory_fts.id = c.id\n WHERE memory_fts MATCH ?\n ${whereClause}\n ORDER BY bm25_score\n LIMIT ?\n `;\n\n let rows: Array<{\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n snippet: string;\n tier: string;\n source: string;\n bm25_score: number;\n }>;\n\n try {\n rows = db.prepare(sql).all(...params) as typeof rows;\n } catch {\n // FTS5 MATCH throws when the query is invalid — return empty results\n return [];\n }\n\n const minScore = opts?.minScore ?? 0.0;\n\n return rows\n .map((row) => ({\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.snippet,\n // Negate so higher = better match for callers\n score: -row.bm25_score,\n tier: row.tier,\n source: row.source,\n }))\n .filter((r) => r.score >= minScore);\n}\n\n// ---------------------------------------------------------------------------\n// Semantic search\n// ---------------------------------------------------------------------------\n\n/**\n * Search chunks using brute-force cosine similarity over stored embeddings.\n *\n * Only chunks that have a non-null embedding BLOB are considered. Chunks\n * without embeddings are silently skipped (they can be embedded later via\n * `embedChunks()`).\n *\n * @param queryEmbedding Pre-computed Float32Array for the search query.\n */\nexport function searchMemorySemantic(\n db: Database,\n queryEmbedding: Float32Array,\n opts?: SearchOptions,\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n\n // Build the SQL filter conditions\n const conditions: string[] = [\"embedding IS NOT NULL\"];\n const params: (string | number)[] = [];\n\n if (opts?.projectIds && opts.projectIds.length > 0) {\n const placeholders = opts.projectIds.map(() => \"?\").join(\", \");\n conditions.push(`project_id IN (${placeholders})`);\n params.push(...opts.projectIds);\n }\n\n if (opts?.sources && opts.sources.length > 0) {\n const placeholders = opts.sources.map(() => \"?\").join(\", \");\n conditions.push(`source IN (${placeholders})`);\n params.push(...opts.sources);\n }\n\n if (opts?.tiers && opts.tiers.length > 0) {\n const placeholders = opts.tiers.map(() => \"?\").join(\", \");\n conditions.push(`tier IN (${placeholders})`);\n params.push(...opts.tiers);\n }\n\n const where = \"WHERE \" + conditions.join(\" AND \");\n\n // Hard cap for SQLite semantic path — prevents OOM on large corpora.\n // Use Postgres for production semantic search.\n const sql = `\n SELECT id, project_id, path, start_line, end_line, text, tier, source, embedding\n FROM memory_chunks\n ${where}\n LIMIT 5000\n `;\n\n const rows = db.prepare(sql).all(...params) as Array<{\n id: string;\n project_id: number;\n path: string;\n start_line: number;\n end_line: number;\n text: string;\n tier: string;\n source: string;\n embedding: Buffer;\n }>;\n\n if (rows.length === 0) return [];\n\n // Compute cosine similarity for every chunk\n const scored = rows.map((row) => {\n const vec = deserializeEmbedding(row.embedding);\n const score = cosineSimilarity(queryEmbedding, vec);\n return {\n projectId: row.project_id,\n path: row.path,\n startLine: row.start_line,\n endLine: row.end_line,\n snippet: row.text,\n score,\n tier: row.tier,\n source: row.source,\n };\n });\n\n // Sort by descending similarity, apply optional min score filter, limit\n const minScore = opts?.minScore ?? -Infinity;\n\n return scored\n .filter((r) => r.score >= minScore)\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults);\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search\n// ---------------------------------------------------------------------------\n\n/**\n * Combine BM25 keyword search and semantic search using normalized scores.\n *\n * Both score sets are min-max normalized to [0,1] before combining, so neither\n * dominates the other regardless of their raw scales.\n *\n * @param queryEmbedding Pre-computed embedding for the query.\n * @param keywordWeight Weight for BM25 score (default 0.5).\n * @param semanticWeight Weight for cosine similarity score (default 0.5).\n */\nexport function searchMemoryHybrid(\n db: Database,\n query: string,\n queryEmbedding: Float32Array,\n opts?: SearchOptions & { keywordWeight?: number; semanticWeight?: number },\n): SearchResult[] {\n const maxResults = opts?.maxResults ?? 10;\n const kw = opts?.keywordWeight ?? 0.5;\n const sw = opts?.semanticWeight ?? 0.5;\n\n // Fetch keyword results — 50 candidates is sufficient for min-max normalization\n const keywordResults = searchMemory(db, query, {\n ...opts,\n maxResults: 50,\n });\n\n // Fetch semantic results — 50 candidates is sufficient for min-max normalization\n const semanticResults = searchMemorySemantic(db, queryEmbedding, {\n ...opts,\n maxResults: 50,\n });\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n // Build a map of chunk ID → combined result\n // Use \"projectId:path:startLine:endLine\" as a stable key (same as chunk IDs)\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n // Min-max normalize helper\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n // Union of all chunk keys\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n // Build a lookup from key → result metadata\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n // Combine scores\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = kw * kwScore + sw * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n // Sort by combined score descending\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n\n// ---------------------------------------------------------------------------\n// Slug lookup helper\n// ---------------------------------------------------------------------------\n\n/**\n * Populate the projectSlug field on search results by looking up project IDs\n * in the registry database.\n */\nexport function populateSlugs(\n results: SearchResult[],\n registryDb: Database,\n): SearchResult[] {\n if (results.length === 0) return results;\n\n const ids = [...new Set(results.map((r) => r.projectId))];\n const placeholders = ids.map(() => \"?\").join(\", \");\n const rows = registryDb\n .prepare(`SELECT id, slug FROM projects WHERE id IN (${placeholders})`)\n .all(...ids) as Array<{ id: number; slug: string }>;\n\n const slugMap = new Map(rows.map((r) => [r.id, r.slug]));\n\n return results.map((r) => ({\n ...r,\n projectSlug: slugMap.get(r.projectId),\n }));\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tools-CUg0Lyg-.mjs","names":[],"sources":["../src/mcp/tools.ts"],"sourcesContent":["/**\n * PAI Knowledge OS — Pure tool handler functions (shared by daemon + legacy MCP server)\n *\n * Each function accepts pre-opened database handles and raw params, executes\n * the tool logic, and returns an MCP-style content array.\n *\n * This module does NOT import indexAll() — indexing is handled by the daemon\n * on its own schedule. The search hot path is pure DB read.\n */\n\nimport { readFileSync, existsSync, statSync } from \"node:fs\";\nimport { join, resolve, isAbsolute } from \"node:path\";\nimport type { Database } from \"better-sqlite3\";\nimport { populateSlugs, searchMemoryHybrid } from \"../memory/search.js\";\nimport { detectProject, formatDetectionJson } from \"../cli/commands/detect.js\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { NotificationMode, NotificationEvent } from \"../notifications/types.js\";\n\n// ---------------------------------------------------------------------------\n// Shared types\n// ---------------------------------------------------------------------------\n\nexport interface ToolContent {\n type: \"text\";\n text: string;\n}\n\nexport interface ToolResult {\n content: ToolContent[];\n isError?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: lookup project_id by slug (also checks aliases)\n// ---------------------------------------------------------------------------\n\nexport function lookupProjectId(\n registryDb: Database,\n slug: string\n): number | null {\n const bySlug = registryDb\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(slug) as { id: number } | undefined;\n if (bySlug) return bySlug.id;\n\n const byAlias = registryDb\n .prepare(\"SELECT project_id FROM aliases WHERE alias = ?\")\n .get(slug) as { project_id: number } | undefined;\n if (byAlias) return byAlias.project_id;\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: detect project from a filesystem path\n// ---------------------------------------------------------------------------\n\ninterface ProjectRow {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n created_at: number;\n updated_at: number;\n archived_at?: number | null;\n parent_id?: number | null;\n obsidian_link?: string | null;\n}\n\nexport function detectProjectFromPath(\n registryDb: Database,\n fsPath: string\n): ProjectRow | null {\n const resolved = resolve(fsPath);\n\n const exact = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects WHERE root_path = ?\"\n )\n .get(resolved) as ProjectRow | undefined;\n\n if (exact) return exact;\n\n const all = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at FROM projects ORDER BY LENGTH(root_path) DESC\"\n )\n .all() as ProjectRow[];\n\n for (const project of all) {\n if (\n resolved.startsWith(project.root_path + \"/\") ||\n resolved === project.root_path\n ) {\n return project;\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: format project row for tool output\n// ---------------------------------------------------------------------------\n\nexport function formatProject(registryDb: Database, project: ProjectRow): string {\n const sessionCount = (\n registryDb\n .prepare(\"SELECT COUNT(*) AS n FROM sessions WHERE project_id = ?\")\n .get(project.id) as { n: number }\n ).n;\n\n const lastSession = registryDb\n .prepare(\n \"SELECT date FROM sessions WHERE project_id = ? ORDER BY date DESC LIMIT 1\"\n )\n .get(project.id) as { date: string } | undefined;\n\n const tags = (\n registryDb\n .prepare(\n `SELECT t.name FROM tags t\n JOIN project_tags pt ON pt.tag_id = t.id\n WHERE pt.project_id = ?\n ORDER BY t.name`\n )\n .all(project.id) as Array<{ name: string }>\n ).map((r) => r.name);\n\n const aliases = (\n registryDb\n .prepare(\"SELECT alias FROM aliases WHERE project_id = ? ORDER BY alias\")\n .all(project.id) as Array<{ alias: string }>\n ).map((r) => r.alias);\n\n const lines: string[] = [\n `slug: ${project.slug}`,\n `display_name: ${project.display_name}`,\n `root_path: ${project.root_path}`,\n `type: ${project.type}`,\n `status: ${project.status}`,\n `sessions: ${sessionCount}`,\n ];\n\n if (lastSession) lines.push(`last_session: ${lastSession.date}`);\n if (tags.length) lines.push(`tags: ${tags.join(\", \")}`);\n if (aliases.length) lines.push(`aliases: ${aliases.join(\", \")}`);\n if (project.obsidian_link) lines.push(`obsidian_link: ${project.obsidian_link}`);\n if (project.archived_at) {\n lines.push(\n `archived_at: ${new Date(project.archived_at).toISOString().slice(0, 10)}`\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_search\n// ---------------------------------------------------------------------------\n\nexport interface MemorySearchParams {\n query: string;\n project?: string;\n all_projects?: boolean;\n sources?: Array<\"memory\" | \"notes\">;\n limit?: number;\n mode?: \"keyword\" | \"semantic\" | \"hybrid\";\n /** Maximum characters per result snippet. Default 200.\n * Limit context consumption — MCP results go into Claude's context window. */\n snippetLength?: number;\n}\n\nexport async function toolMemorySearch(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: MemorySearchParams\n): Promise<ToolResult> {\n try {\n const projectIds: number[] | undefined = params.project\n ? (() => {\n const id = lookupProjectId(registryDb, params.project!);\n return id != null ? [id] : [];\n })()\n : undefined;\n\n if (params.project && (!projectIds || projectIds.length === 0)) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n // NOTE: No indexAll() here — indexing is handled by the daemon scheduler.\n // The daemon ensures the index stays fresh; the search hot path is read-only.\n\n const mode = params.mode ?? \"keyword\";\n // Limit context consumption — MCP results go into Claude's context window.\n // Default to 5 results and 200-char snippets to keep a single search call\n // within ~1-2K tokens rather than 5K+.\n const snippetLength = params.snippetLength ?? 200;\n const searchOpts = {\n projectIds,\n sources: params.sources,\n maxResults: params.limit ?? 5,\n };\n\n let results;\n\n // Determine if federation is a StorageBackend or a raw Database\n const isBackend = (x: Database | StorageBackend): x is StorageBackend =>\n \"backendType\" in x;\n\n if (isBackend(federation)) {\n // Use the storage backend interface (works for both SQLite and Postgres)\n if (mode === \"keyword\") {\n results = await federation.searchKeyword(params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const { generateEmbedding } = await import(\"../memory/embeddings.js\");\n const queryEmbedding = await generateEmbedding(params.query, true);\n\n if (mode === \"semantic\") {\n results = await federation.searchSemantic(queryEmbedding, searchOpts);\n } else {\n // Hybrid: combine keyword + semantic\n const [kwResults, semResults] = await Promise.all([\n federation.searchKeyword(params.query, { ...searchOpts, maxResults: 50 }),\n federation.searchSemantic(queryEmbedding, { ...searchOpts, maxResults: 50 }),\n ]); // 50 candidates is sufficient for min-max normalization\n // Reuse the existing hybrid scoring logic\n results = combineHybridResults(kwResults, semResults, searchOpts.maxResults ?? 10);\n }\n } else {\n results = await federation.searchKeyword(params.query, searchOpts);\n }\n } else {\n // Legacy path: raw better-sqlite3 Database (for direct MCP server usage)\n const { searchMemory, searchMemorySemantic } = await import(\"../memory/search.js\");\n\n if (mode === \"keyword\") {\n results = searchMemory(federation, params.query, searchOpts);\n } else if (mode === \"semantic\" || mode === \"hybrid\") {\n const { generateEmbedding } = await import(\"../memory/embeddings.js\");\n const queryEmbedding = await generateEmbedding(params.query, true);\n\n if (mode === \"semantic\") {\n results = searchMemorySemantic(federation, queryEmbedding, searchOpts);\n } else {\n results = searchMemoryHybrid(\n federation,\n params.query,\n queryEmbedding,\n searchOpts\n );\n }\n } else {\n results = searchMemory(federation, params.query, searchOpts);\n }\n }\n\n const withSlugs = populateSlugs(results, registryDb);\n\n if (withSlugs.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No results found for query: \"${params.query}\" (mode: ${mode})`,\n },\n ],\n };\n }\n\n const formatted = withSlugs\n .map((r, i) => {\n const header = `[${i + 1}] ${r.projectSlug ?? `project:${r.projectId}`} — ${r.path} (lines ${r.startLine}-${r.endLine}) score=${r.score.toFixed(4)} tier=${r.tier} source=${r.source}`;\n // Truncate snippet to snippetLength — limit context consumption.\n // MCP results go into Claude's context window; keep each result tight.\n const raw = r.snippet.trim();\n const snippet = raw.length > snippetLength\n ? raw.slice(0, snippetLength) + \"...\"\n : raw;\n return `${header}\\n${snippet}`;\n })\n .join(\"\\n\\n---\\n\\n\");\n\n return {\n content: [\n {\n type: \"text\",\n text: `Found ${withSlugs.length} result(s) for \"${params.query}\" (mode: ${mode}):\\n\\n${formatted}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Search error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: memory_get\n// ---------------------------------------------------------------------------\n\nexport interface MemoryGetParams {\n project: string;\n path: string;\n from?: number;\n lines?: number;\n}\n\nexport function toolMemoryGet(\n registryDb: Database,\n params: MemoryGetParams\n): ToolResult {\n try {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const project = registryDb\n .prepare(\"SELECT root_path FROM projects WHERE id = ?\")\n .get(projectId) as { root_path: string } | undefined;\n\n if (!project) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const requestedPath = params.path;\n if (requestedPath.includes(\"..\") || isAbsolute(requestedPath)) {\n return {\n content: [\n {\n type: \"text\",\n text: `Invalid path: ${params.path} (must be a relative path within the project root, no ../ allowed)`,\n },\n ],\n isError: true,\n };\n }\n\n const fullPath = join(project.root_path, requestedPath);\n const resolvedFull = resolve(fullPath);\n const resolvedRoot = resolve(project.root_path);\n\n if (\n !resolvedFull.startsWith(resolvedRoot + \"/\") &&\n resolvedFull !== resolvedRoot\n ) {\n return {\n content: [\n { type: \"text\", text: `Path traversal blocked: ${params.path}` },\n ],\n isError: true,\n };\n }\n\n if (!existsSync(fullPath)) {\n return {\n content: [\n {\n type: \"text\",\n text: `File not found: ${requestedPath} (project: ${params.project})`,\n },\n ],\n isError: true,\n };\n }\n\n const stat = statSync(fullPath);\n if (stat.size > 5 * 1024 * 1024) {\n return {\n content: [\n {\n type: \"text\",\n text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum 5 MB.`,\n },\n ],\n };\n }\n\n const content = readFileSync(fullPath, \"utf8\");\n const allLines = content.split(\"\\n\");\n\n const fromLine = (params.from ?? 1) - 1;\n const toLine =\n params.lines != null\n ? Math.min(fromLine + params.lines, allLines.length)\n : allLines.length;\n\n const selectedLines = allLines.slice(fromLine, toLine);\n const text = selectedLines.join(\"\\n\");\n\n const header =\n params.from != null\n ? `${params.project}/${requestedPath} (lines ${fromLine + 1}-${toLine}):`\n : `${params.project}/${requestedPath}:`;\n\n return {\n content: [{ type: \"text\", text: `${header}\\n\\n${text}` }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `Read error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_info\n// ---------------------------------------------------------------------------\n\nexport interface ProjectInfoParams {\n slug?: string;\n}\n\nexport function toolProjectInfo(\n registryDb: Database,\n params: ProjectInfoParams\n): ToolResult {\n try {\n let project: ProjectRow | null = null;\n\n if (params.slug) {\n const projectId = lookupProjectId(registryDb, params.slug);\n if (projectId != null) {\n project = registryDb\n .prepare(\n \"SELECT id, slug, display_name, root_path, type, status, created_at, updated_at, archived_at, parent_id, obsidian_link FROM projects WHERE id = ?\"\n )\n .get(projectId) as ProjectRow | null;\n }\n } else {\n const cwd = process.cwd();\n project = detectProjectFromPath(registryDb, cwd);\n }\n\n if (!project) {\n const message = params.slug\n ? `Project not found: ${params.slug}`\n : `No PAI project found matching the current directory: ${process.cwd()}`;\n return {\n content: [{ type: \"text\", text: message }],\n isError: !params.slug,\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatProject(registryDb, project) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_info error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_list\n// ---------------------------------------------------------------------------\n\nexport interface ProjectListParams {\n status?: \"active\" | \"archived\" | \"migrating\";\n tag?: string;\n limit?: number;\n}\n\nexport function toolProjectList(\n registryDb: Database,\n params: ProjectListParams\n): ToolResult {\n try {\n const conditions: string[] = [];\n const queryParams: (string | number)[] = [];\n\n if (params.status) {\n conditions.push(\"p.status = ?\");\n queryParams.push(params.status);\n }\n\n if (params.tag) {\n conditions.push(\n \"p.id IN (SELECT pt.project_id FROM project_tags pt JOIN tags t ON pt.tag_id = t.id WHERE t.name = ?)\"\n );\n queryParams.push(params.tag);\n }\n\n const where =\n conditions.length > 0 ? `WHERE ${conditions.join(\" AND \")}` : \"\";\n const limit = params.limit ?? 50;\n queryParams.push(limit);\n\n const projects = registryDb\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.root_path, p.type, p.status, p.updated_at\n FROM projects p\n ${where}\n ORDER BY p.updated_at DESC\n LIMIT ?`\n )\n .all(...queryParams) as Array<{\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n updated_at: number;\n }>;\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No projects found matching the given filters.\",\n },\n ],\n };\n }\n\n const lines = projects.map(\n (p) =>\n `${p.slug} [${p.status}] ${p.root_path} (updated: ${new Date(p.updated_at).toISOString().slice(0, 10)})`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} project(s):\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_list\n// ---------------------------------------------------------------------------\n\nexport interface SessionListParams {\n project: string;\n limit?: number;\n status?: \"open\" | \"completed\" | \"compacted\";\n}\n\nexport function toolSessionList(\n registryDb: Database,\n params: SessionListParams\n): ToolResult {\n try {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const conditions = [\"project_id = ?\"];\n const queryParams: (string | number)[] = [projectId];\n\n if (params.status) {\n conditions.push(\"status = ?\");\n queryParams.push(params.status);\n }\n\n const limit = params.limit ?? 10;\n queryParams.push(limit);\n\n const sessions = registryDb\n .prepare(\n `SELECT number, date, title, filename, status\n FROM sessions\n WHERE ${conditions.join(\" AND \")}\n ORDER BY number DESC\n LIMIT ?`\n )\n .all(...queryParams) as Array<{\n number: number;\n date: string;\n title: string;\n filename: string;\n status: string;\n }>;\n\n if (sessions.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No sessions found for project: ${params.project}`,\n },\n ],\n };\n }\n\n const lines = sessions.map(\n (s) =>\n `#${String(s.number).padStart(4, \"0\")} ${s.date} [${s.status}] ${s.title}\\n file: Notes/${s.filename}`\n );\n\n return {\n content: [\n {\n type: \"text\",\n text: `${sessions.length} session(s) for ${params.project}:\\n\\n${lines.join(\"\\n\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_list error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: registry_search\n// ---------------------------------------------------------------------------\n\nexport interface RegistrySearchParams {\n query: string;\n}\n\nexport function toolRegistrySearch(\n registryDb: Database,\n params: RegistrySearchParams\n): ToolResult {\n try {\n const q = `%${params.query}%`;\n const projects = registryDb\n .prepare(\n `SELECT id, slug, display_name, root_path, type, status, updated_at\n FROM projects\n WHERE slug LIKE ?\n OR display_name LIKE ?\n OR root_path LIKE ?\n ORDER BY updated_at DESC\n LIMIT 20`\n )\n .all(q, q, q) as Array<{\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n type: string;\n status: string;\n updated_at: number;\n }>;\n\n if (projects.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: `No projects found matching: \"${params.query}\"`,\n },\n ],\n };\n }\n\n const lines = projects.map((p) => `${p.slug} [${p.status}] ${p.root_path}`);\n\n return {\n content: [\n {\n type: \"text\",\n text: `${projects.length} match(es) for \"${params.query}\":\\n\\n${lines.join(\"\\n\")}`,\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `registry_search error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_detect\n// ---------------------------------------------------------------------------\n\nexport interface ProjectDetectParams {\n cwd?: string;\n}\n\nexport function toolProjectDetect(\n registryDb: Database,\n params: ProjectDetectParams\n): ToolResult {\n try {\n const detection = detectProject(registryDb, params.cwd);\n\n if (!detection) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: `No registered project found for path: ${target}\\n\\nRun 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatDetectionJson(detection) }],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_detect error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_health\n// ---------------------------------------------------------------------------\n\nexport interface ProjectHealthParams {\n category?: \"active\" | \"stale\" | \"dead\" | \"all\";\n}\n\nexport async function toolProjectHealth(\n registryDb: Database,\n params: ProjectHealthParams\n): Promise<ToolResult> {\n try {\n const { existsSync: fsExists, readdirSync, statSync } = await import(\n \"node:fs\"\n );\n const {\n join: pathJoin,\n basename: pathBasename,\n } = await import(\"node:path\");\n const { homedir } = await import(\"node:os\");\n const { encodeDir: enc } = await import(\"../cli/utils.js\");\n\n interface HealthRowLocal {\n id: number;\n slug: string;\n display_name: string;\n root_path: string;\n encoded_dir: string;\n status: string;\n type: string;\n session_count: number;\n }\n\n const rows = registryDb\n .prepare(\n `SELECT p.id, p.slug, p.display_name, p.root_path, p.encoded_dir, p.status, p.type,\n (SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count\n FROM projects p\n ORDER BY p.slug ASC`\n )\n .all() as HealthRowLocal[];\n\n const home = homedir();\n const claudeProjects = pathJoin(home, \".claude\", \"projects\");\n\n function suggestMoved(rootPath: string): string | undefined {\n const name = pathBasename(rootPath);\n const candidates = [\n pathJoin(home, \"dev\", name),\n pathJoin(home, \"dev\", \"ai\", name),\n pathJoin(home, \"Desktop\", name),\n pathJoin(home, \"Projects\", name),\n ];\n return candidates.find((c) => fsExists(c));\n }\n\n function hasClaudeNotes(encodedDir: string): boolean {\n if (!fsExists(claudeProjects)) return false;\n try {\n for (const entry of readdirSync(claudeProjects)) {\n if (entry !== encodedDir && !entry.startsWith(encodedDir)) continue;\n const full = pathJoin(claudeProjects, entry);\n try {\n if (!statSync(full).isDirectory()) continue;\n } catch {\n continue;\n }\n if (fsExists(pathJoin(full, \"Notes\"))) return true;\n }\n } catch {\n /* ignore */\n }\n return false;\n }\n\n interface HealthResult {\n slug: string;\n display_name: string;\n root_path: string;\n status: string;\n type: string;\n session_count: number;\n health: string;\n suggested_path: string | null;\n has_claude_notes: boolean;\n todo: {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n };\n }\n\n function findTodoForProject(rootPath: string): {\n found: boolean;\n path: string | null;\n has_continue: boolean;\n } {\n const locs = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n ];\n for (const rel of locs) {\n const full = pathJoin(rootPath, rel);\n if (fsExists(full)) {\n try {\n const raw = readFileSync(full, \"utf8\");\n const hasContinue = /^## Continue$/m.test(raw);\n return { found: true, path: rel, has_continue: hasContinue };\n } catch {\n return { found: true, path: rel, has_continue: false };\n }\n }\n }\n return { found: false, path: null, has_continue: false };\n }\n\n const results: HealthResult[] = rows.map((p) => {\n const pathExists = fsExists(p.root_path);\n let health: string;\n let suggestedPath: string | null = null;\n\n if (pathExists) {\n health = \"active\";\n } else {\n suggestedPath = suggestMoved(p.root_path) ?? null;\n health = suggestedPath ? \"stale\" : \"dead\";\n }\n\n const todo = pathExists\n ? findTodoForProject(p.root_path)\n : { found: false, path: null, has_continue: false };\n\n return {\n slug: p.slug,\n display_name: p.display_name,\n root_path: p.root_path,\n status: p.status,\n type: p.type,\n session_count: p.session_count,\n health,\n suggested_path: suggestedPath,\n has_claude_notes: hasClaudeNotes(p.encoded_dir),\n todo,\n };\n });\n\n const filtered =\n !params.category || params.category === \"all\"\n ? results\n : results.filter((r) => r.health === params.category);\n\n const summary = {\n total: rows.length,\n active: results.filter((r) => r.health === \"active\").length,\n stale: results.filter((r) => r.health === \"stale\").length,\n dead: results.filter((r) => r.health === \"dead\").length,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify({ summary, projects: filtered }, null, 2),\n },\n ],\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `project_health error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: project_todo\n// ---------------------------------------------------------------------------\n\nexport interface ProjectTodoParams {\n project?: string;\n}\n\n/**\n * TODO candidate locations searched in priority order.\n * Returns the first one that exists, along with its label.\n */\nconst TODO_LOCATIONS = [\n { rel: \"Notes/TODO.md\", label: \"Notes/TODO.md\" },\n { rel: \".claude/Notes/TODO.md\", label: \".claude/Notes/TODO.md\" },\n { rel: \"tasks/todo.md\", label: \"tasks/todo.md\" },\n { rel: \"TODO.md\", label: \"TODO.md\" },\n];\n\n/**\n * Given TODO file content, extract and surface the ## Continue section first,\n * then return the remaining content. Returns an object with:\n * continueSection: string | null\n * fullContent: string\n * hasContinue: boolean\n */\nfunction parseTodoContent(raw: string): {\n continueSection: string | null;\n fullContent: string;\n hasContinue: boolean;\n} {\n const lines = raw.split(\"\\n\");\n\n // Find the ## Continue heading\n const continueIdx = lines.findIndex(\n (l) => l.trim() === \"## Continue\"\n );\n\n if (continueIdx === -1) {\n return { continueSection: null, fullContent: raw, hasContinue: false };\n }\n\n // The section ends at the first `---` separator or next `##` heading after\n // the Continue heading (whichever comes first).\n let endIdx = lines.length;\n for (let i = continueIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === \"---\" || (trimmed.startsWith(\"##\") && trimmed !== \"## Continue\")) {\n endIdx = i;\n break;\n }\n }\n\n const continueLines = lines.slice(continueIdx, endIdx);\n const continueSection = continueLines.join(\"\\n\").trim();\n\n return { continueSection, fullContent: raw, hasContinue: true };\n}\n\nexport function toolProjectTodo(\n registryDb: Database,\n params: ProjectTodoParams\n): ToolResult {\n try {\n let rootPath: string;\n let projectSlug: string;\n\n if (params.project) {\n const projectId = lookupProjectId(registryDb, params.project);\n if (projectId == null) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n const row = registryDb\n .prepare(\"SELECT root_path, slug FROM projects WHERE id = ?\")\n .get(projectId) as { root_path: string; slug: string } | undefined;\n\n if (!row) {\n return {\n content: [\n { type: \"text\", text: `Project not found: ${params.project}` },\n ],\n isError: true,\n };\n }\n\n rootPath = row.root_path;\n projectSlug = row.slug;\n } else {\n // Auto-detect from cwd\n const project = detectProjectFromPath(registryDb, process.cwd());\n if (!project) {\n return {\n content: [\n {\n type: \"text\",\n text: `No PAI project found matching the current directory: ${process.cwd()}\\n\\nProvide a project slug or run 'pai project add .' to register this directory.`,\n },\n ],\n };\n }\n rootPath = project.root_path;\n projectSlug = project.slug;\n }\n\n // Search for TODO in priority order\n for (const loc of TODO_LOCATIONS) {\n const fullPath = join(rootPath, loc.rel);\n if (existsSync(fullPath)) {\n const raw = readFileSync(fullPath, \"utf8\");\n const { continueSection, fullContent, hasContinue } = parseTodoContent(raw);\n\n let output: string;\n if (hasContinue && continueSection) {\n // Surface the ## Continue section first, then the full content\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n \"=== CONTINUE SECTION (surfaced first) ===\",\n continueSection,\n \"\",\n \"=== FULL TODO CONTENT ===\",\n fullContent,\n ].join(\"\\n\");\n } else {\n output = [\n `TODO found: ${projectSlug}/${loc.label}`,\n \"\",\n fullContent,\n ].join(\"\\n\");\n }\n\n return {\n content: [{ type: \"text\", text: output }],\n };\n }\n }\n\n // No TODO found in any location\n const searched = TODO_LOCATIONS.map((l) => ` ${rootPath}/${l.rel}`).join(\"\\n\");\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No TODO.md found for project: ${projectSlug}`,\n \"\",\n \"Searched locations (in order):\",\n searched,\n \"\",\n \"Create a TODO with: echo '## Tasks\\\\n- [ ] First task' > Notes/TODO.md\",\n ].join(\"\\n\"),\n },\n ],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `project_todo error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: notification_config\n// ---------------------------------------------------------------------------\n\nexport interface NotificationConfigParams {\n /** Action to perform */\n action: \"get\" | \"set\" | \"send\";\n /** For action=\"set\": the notification mode to activate */\n mode?: NotificationMode;\n /** For action=\"set\": partial channel config overrides (JSON object) */\n channels?: Record<string, unknown>;\n /** For action=\"set\": partial routing overrides (JSON object) */\n routing?: Record<string, unknown>;\n /** For action=\"send\": the event type */\n event?: NotificationEvent;\n /** For action=\"send\": the notification message */\n message?: string;\n /** For action=\"send\": optional title */\n title?: string;\n}\n\n/**\n * Handle notification config queries and updates via the daemon IPC.\n * Falls back gracefully if the daemon is not running.\n */\nexport async function toolNotificationConfig(\n params: NotificationConfigParams\n): Promise<ToolResult> {\n try {\n const { PaiClient } = await import(\"../daemon/ipc-client.js\");\n const client = new PaiClient();\n\n if (params.action === \"get\") {\n const { config, activeChannels } = await client.getNotificationConfig();\n const lines = [\n `mode: ${config.mode}`,\n `active_channels: ${activeChannels.join(\", \") || \"(none)\"}`,\n \"\",\n \"channels:\",\n ...Object.entries(config.channels).map(([ch, cfg]) => {\n const c = cfg as { enabled: boolean };\n return ` ${ch}: ${c.enabled ? \"enabled\" : \"disabled\"}`;\n }),\n \"\",\n \"routing:\",\n ...Object.entries(config.routing).map(\n ([event, channels]) => ` ${event}: ${(channels as string[]).join(\", \") || \"(none)\"}`\n ),\n ];\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n }\n\n if (params.action === \"set\") {\n if (!params.mode && !params.channels && !params.routing) {\n return {\n content: [\n {\n type: \"text\",\n text: \"notification_config set: provide at least one of mode, channels, or routing.\",\n },\n ],\n isError: true,\n };\n }\n const result = await client.setNotificationConfig({\n mode: params.mode,\n channels: params.channels as Parameters<typeof client.setNotificationConfig>[0][\"channels\"],\n routing: params.routing as Parameters<typeof client.setNotificationConfig>[0][\"routing\"],\n });\n return {\n content: [\n {\n type: \"text\",\n text: `Notification config updated. Mode: ${result.config.mode}`,\n },\n ],\n };\n }\n\n if (params.action === \"send\") {\n if (!params.message) {\n return {\n content: [\n { type: \"text\", text: \"notification_config send: message is required.\" },\n ],\n isError: true,\n };\n }\n const result = await client.sendNotification({\n event: params.event ?? \"info\",\n message: params.message,\n title: params.title,\n });\n const lines = [\n `mode: ${result.mode}`,\n `attempted: ${result.channelsAttempted.join(\", \") || \"(none)\"}`,\n `succeeded: ${result.channelsSucceeded.join(\", \") || \"(none)\"}`,\n ...(result.channelsFailed.length > 0\n ? [`failed: ${result.channelsFailed.join(\", \")}`]\n : []),\n ];\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Unknown action: ${String(params.action)}. Use \"get\", \"set\", or \"send\".`,\n },\n ],\n isError: true,\n };\n } catch (e) {\n return {\n content: [\n { type: \"text\", text: `notification_config error: ${String(e)}` },\n ],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: topic_detect\n// ---------------------------------------------------------------------------\n\nexport interface TopicDetectParams {\n /** Recent conversation context (a few sentences summarising recent activity) */\n context: string;\n /** The project slug the session is currently routed to. */\n current_project?: string;\n /**\n * Minimum confidence [0,1] to declare a shift. Default: 0.6.\n * Higher = less sensitive (fewer false positives).\n */\n threshold?: number;\n}\n\n/**\n * Detect whether recent conversation context has shifted to a different project.\n * Uses memory_search to find which project best matches the context, then\n * compares against the current project.\n *\n * Calls the daemon via IPC so it has access to the storage backend.\n * Falls back gracefully if the daemon is not running.\n */\nexport async function toolTopicDetect(\n params: TopicDetectParams\n): Promise<ToolResult> {\n try {\n const { PaiClient } = await import(\"../daemon/ipc-client.js\");\n const client = new PaiClient();\n\n const result = await client.topicCheck({\n context: params.context,\n currentProject: params.current_project,\n threshold: params.threshold,\n });\n\n const lines: string[] = [\n `shifted: ${result.shifted}`,\n `current_project: ${result.currentProject ?? \"(none)\"}`,\n `suggested_project: ${result.suggestedProject ?? \"(none)\"}`,\n `confidence: ${result.confidence.toFixed(3)}`,\n `chunks_scored: ${result.chunkCount}`,\n ];\n\n if (result.topProjects.length > 0) {\n lines.push(\"\");\n lines.push(\"top_matches:\");\n for (const p of result.topProjects) {\n lines.push(` ${p.slug}: ${(p.score * 100).toFixed(1)}%`);\n }\n }\n\n if (result.shifted) {\n lines.push(\"\");\n lines.push(\n `TOPIC SHIFT DETECTED: conversation appears to be about \"${result.suggestedProject}\" ` +\n `(confidence: ${(result.confidence * 100).toFixed(0)}%), not \"${result.currentProject}\".`\n );\n }\n\n return {\n content: [{ type: \"text\", text: lines.join(\"\\n\") }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `topic_detect error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: session_route\n// ---------------------------------------------------------------------------\n\nexport interface SessionRouteParams {\n /** Working directory to route from (defaults to process.cwd()) */\n cwd?: string;\n /** Optional conversation context for topic-based fallback routing */\n context?: string;\n}\n\n/**\n * Automatically suggest which project a session belongs to.\n *\n * Strategy (in priority order):\n * 1. path — exact or parent-directory match in the project registry\n * 2. marker — walk up from cwd looking for Notes/PAI.md\n * 3. topic — BM25 keyword search against memory (requires context)\n *\n * Call this at session start (e.g., from CLAUDE.md or a session-start hook)\n * to automatically route the session to the correct project.\n */\nexport async function toolSessionRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n params: SessionRouteParams\n): Promise<ToolResult> {\n try {\n const { autoRoute, formatAutoRouteJson } = await import(\"../session/auto-route.js\");\n\n const result = await autoRoute(\n registryDb,\n federation,\n params.cwd,\n params.context\n );\n\n if (!result) {\n const target = params.cwd ?? process.cwd();\n return {\n content: [\n {\n type: \"text\",\n text: [\n `No project match found for: ${target}`,\n \"\",\n \"Tried: path match, PAI.md marker walk\" +\n (params.context ? \", topic detection\" : \"\"),\n \"\",\n \"Run 'pai project add .' to register this directory,\",\n \"or provide conversation context for topic-based routing.\",\n ].join(\"\\n\"),\n },\n ],\n };\n }\n\n return {\n content: [{ type: \"text\", text: formatAutoRouteJson(result) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `session_route error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_explore\n// ---------------------------------------------------------------------------\n\nexport interface ZettelExploreParams {\n start_note: string;\n depth?: number;\n direction?: string;\n mode?: string;\n}\n\nexport async function toolZettelExplore(\n federationDb: Database,\n params: ZettelExploreParams\n): Promise<ToolResult> {\n try {\n const { zettelExplore } = await import(\"../zettelkasten/index.js\");\n const result = zettelExplore(federationDb, {\n startNote: params.start_note,\n depth: params.depth,\n direction: params.direction as \"forward\" | \"backward\" | \"both\" | undefined,\n mode: params.mode as \"sequential\" | \"associative\" | \"all\" | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_explore error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_health\n// ---------------------------------------------------------------------------\n\nexport interface ZettelHealthParams {\n scope?: string;\n project_path?: string;\n recent_days?: number;\n include?: string[];\n}\n\nexport async function toolZettelHealth(\n federationDb: Database,\n params: ZettelHealthParams\n): Promise<ToolResult> {\n try {\n const { zettelHealth } = await import(\"../zettelkasten/index.js\");\n const result = zettelHealth(federationDb, {\n scope: params.scope as \"full\" | \"recent\" | \"project\" | undefined,\n projectPath: params.project_path,\n recentDays: params.recent_days,\n include: params.include as Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\"> | undefined,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_health error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_surprise\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSurpriseParams {\n reference_path: string;\n vault_project_id: number;\n limit?: number;\n min_similarity?: number;\n min_graph_distance?: number;\n}\n\nexport async function toolZettelSurprise(\n federationDb: Database,\n params: ZettelSurpriseParams\n): Promise<ToolResult> {\n try {\n const { zettelSurprise } = await import(\"../zettelkasten/index.js\");\n const results = await zettelSurprise(federationDb, {\n referencePath: params.reference_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n minSimilarity: params.min_similarity,\n minGraphDistance: params.min_graph_distance,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_surprise error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_suggest\n// ---------------------------------------------------------------------------\n\nexport interface ZettelSuggestParams {\n note_path: string;\n vault_project_id: number;\n limit?: number;\n exclude_linked?: boolean;\n}\n\nexport async function toolZettelSuggest(\n federationDb: Database,\n params: ZettelSuggestParams\n): Promise<ToolResult> {\n try {\n const { zettelSuggest } = await import(\"../zettelkasten/index.js\");\n const results = await zettelSuggest(federationDb, {\n notePath: params.note_path,\n vaultProjectId: params.vault_project_id,\n limit: params.limit,\n excludeLinked: params.exclude_linked,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(results, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_suggest error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_converse\n// ---------------------------------------------------------------------------\n\nexport interface ZettelConverseParams {\n question: string;\n vault_project_id: number;\n depth?: number;\n limit?: number;\n}\n\nexport async function toolZettelConverse(\n federationDb: Database,\n params: ZettelConverseParams\n): Promise<ToolResult> {\n try {\n const { zettelConverse } = await import(\"../zettelkasten/index.js\");\n const result = await zettelConverse(federationDb, {\n question: params.question,\n vaultProjectId: params.vault_project_id,\n depth: params.depth,\n limit: params.limit,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_converse error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tool: zettel_themes\n// ---------------------------------------------------------------------------\n\nexport interface ZettelThemesParams {\n vault_project_id: number;\n lookback_days?: number;\n min_cluster_size?: number;\n max_themes?: number;\n similarity_threshold?: number;\n}\n\nexport async function toolZettelThemes(\n federationDb: Database,\n params: ZettelThemesParams\n): Promise<ToolResult> {\n try {\n const { zettelThemes } = await import(\"../zettelkasten/index.js\");\n const result = await zettelThemes(federationDb, {\n vaultProjectId: params.vault_project_id,\n lookbackDays: params.lookback_days,\n minClusterSize: params.min_cluster_size,\n maxThemes: params.max_themes,\n similarityThreshold: params.similarity_threshold,\n });\n return {\n content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n };\n } catch (e) {\n return {\n content: [{ type: \"text\", text: `zettel_themes error: ${String(e)}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid search helper (backend-agnostic)\n// ---------------------------------------------------------------------------\n\nimport type { SearchResult } from \"../memory/search.js\";\n\n/**\n * Combine keyword + semantic results using min-max normalized scoring.\n * Mirrors the logic in searchMemoryHybrid() from memory/search.ts,\n * but works on pre-computed result arrays so it works for any backend.\n */\nfunction combineHybridResults(\n keywordResults: SearchResult[],\n semanticResults: SearchResult[],\n maxResults: number,\n keywordWeight = 0.5,\n semanticWeight = 0.5\n): SearchResult[] {\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(items: SearchResult[]): Map<string, number> {\n if (items.length === 0) return new Map();\n const min = Math.min(...items.map((r) => r.score));\n const max = Math.max(...items.map((r) => r.score));\n const range = max - min;\n const m = new Map<string, number>();\n for (const r of items) {\n m.set(keyFor(r), range === 0 ? 1 : (r.score - min) / range);\n }\n return m;\n }\n\n const kwNorm = minMaxNormalize(keywordResults);\n const semNorm = minMaxNormalize(semanticResults);\n\n const allKeys = new Set<string>([\n ...keywordResults.map(keyFor),\n ...semanticResults.map(keyFor),\n ]);\n\n const metaMap = new Map<string, SearchResult>();\n for (const r of [...keywordResults, ...semanticResults]) {\n metaMap.set(keyFor(r), r);\n }\n\n const combined: Array<SearchResult & { combinedScore: number }> = [];\n for (const key of allKeys) {\n const meta = metaMap.get(key)!;\n const kwScore = kwNorm.get(key) ?? 0;\n const semScore = semNorm.get(key) ?? 0;\n const combinedScore = keywordWeight * kwScore + semanticWeight * semScore;\n combined.push({ ...meta, score: combinedScore, combinedScore });\n }\n\n return combined\n .sort((a, b) => b.score - a.score)\n .slice(0, maxResults)\n .map(({ combinedScore: _unused, ...r }) => r);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,gBACd,YACA,MACe;CACf,MAAM,SAAS,WACZ,QAAQ,yCAAyC,CACjD,IAAI,KAAK;AACZ,KAAI,OAAQ,QAAO,OAAO;CAE1B,MAAM,UAAU,WACb,QAAQ,iDAAiD,CACzD,IAAI,KAAK;AACZ,KAAI,QAAS,QAAO,QAAQ;AAE5B,QAAO;;AAqBT,SAAgB,sBACd,YACA,QACmB;CACnB,MAAM,WAAW,QAAQ,OAAO;CAEhC,MAAM,QAAQ,WACX,QACC,mHACD,CACA,IAAI,SAAS;AAEhB,KAAI,MAAO,QAAO;CAElB,MAAM,MAAM,WACT,QACC,+HACD,CACA,KAAK;AAER,MAAK,MAAM,WAAW,IACpB,KACE,SAAS,WAAW,QAAQ,YAAY,IAAI,IAC5C,aAAa,QAAQ,UAErB,QAAO;AAIX,QAAO;;AAOT,SAAgB,cAAc,YAAsB,SAA6B;CAC/E,MAAM,eACJ,WACG,QAAQ,0DAA0D,CAClE,IAAI,QAAQ,GAAG,CAClB;CAEF,MAAM,cAAc,WACjB,QACC,4EACD,CACA,IAAI,QAAQ,GAAG;CAElB,MAAM,OACJ,WACG,QACC;;;0BAID,CACA,IAAI,QAAQ,GAAG,CAClB,KAAK,MAAM,EAAE,KAAK;CAEpB,MAAM,UACJ,WACG,QAAQ,gEAAgE,CACxE,IAAI,QAAQ,GAAG,CAClB,KAAK,MAAM,EAAE,MAAM;CAErB,MAAM,QAAkB;EACtB,SAAS,QAAQ;EACjB,iBAAiB,QAAQ;EACzB,cAAc,QAAQ;EACtB,SAAS,QAAQ;EACjB,WAAW,QAAQ;EACnB,aAAa;EACd;AAED,KAAI,YAAa,OAAM,KAAK,iBAAiB,YAAY,OAAO;AAChE,KAAI,KAAK,OAAQ,OAAM,KAAK,SAAS,KAAK,KAAK,KAAK,GAAG;AACvD,KAAI,QAAQ,OAAQ,OAAM,KAAK,YAAY,QAAQ,KAAK,KAAK,GAAG;AAChE,KAAI,QAAQ,cAAe,OAAM,KAAK,kBAAkB,QAAQ,gBAAgB;AAChF,KAAI,QAAQ,YACV,OAAM,KACJ,gBAAgB,IAAI,KAAK,QAAQ,YAAY,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,GACzE;AAGH,QAAO,MAAM,KAAK,KAAK;;AAmBzB,eAAsB,iBACpB,YACA,YACA,QACqB;AACrB,KAAI;EACF,MAAM,aAAmC,OAAO,iBACrC;GACL,MAAM,KAAK,gBAAgB,YAAY,OAAO,QAAS;AACvD,UAAO,MAAM,OAAO,CAAC,GAAG,GAAG,EAAE;MAC3B,GACJ;AAEJ,MAAI,OAAO,YAAY,CAAC,cAAc,WAAW,WAAW,GAC1D,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAMH,MAAM,OAAO,OAAO,QAAQ;EAI5B,MAAM,gBAAgB,OAAO,iBAAiB;EAC9C,MAAM,aAAa;GACjB;GACA,SAAS,OAAO;GAChB,YAAY,OAAO,SAAS;GAC7B;EAED,IAAI;EAGJ,MAAM,aAAa,MACjB,iBAAiB;AAEnB,MAAI,UAAU,WAAW,CAEvB,KAAI,SAAS,UACX,WAAU,MAAM,WAAW,cAAc,OAAO,OAAO,WAAW;WACzD,SAAS,cAAc,SAAS,UAAU;GACnD,MAAM,EAAE,sBAAsB,MAAM,OAAO;GAC3C,MAAM,iBAAiB,MAAM,kBAAkB,OAAO,OAAO,KAAK;AAElE,OAAI,SAAS,WACX,WAAU,MAAM,WAAW,eAAe,gBAAgB,WAAW;QAChE;IAEL,MAAM,CAAC,WAAW,cAAc,MAAM,QAAQ,IAAI,CAChD,WAAW,cAAc,OAAO,OAAO;KAAE,GAAG;KAAY,YAAY;KAAI,CAAC,EACzE,WAAW,eAAe,gBAAgB;KAAE,GAAG;KAAY,YAAY;KAAI,CAAC,CAC7E,CAAC;AAEF,cAAU,qBAAqB,WAAW,YAAY,WAAW,cAAc,GAAG;;QAGpF,WAAU,MAAM,WAAW,cAAc,OAAO,OAAO,WAAW;OAE/D;GAEL,MAAM,EAAE,cAAc,yBAAyB,MAAM,OAAO;AAE5D,OAAI,SAAS,UACX,WAAU,aAAa,YAAY,OAAO,OAAO,WAAW;YACnD,SAAS,cAAc,SAAS,UAAU;IACnD,MAAM,EAAE,sBAAsB,MAAM,OAAO;IAC3C,MAAM,iBAAiB,MAAM,kBAAkB,OAAO,OAAO,KAAK;AAElE,QAAI,SAAS,WACX,WAAU,qBAAqB,YAAY,gBAAgB,WAAW;QAEtE,WAAU,mBACR,YACA,OAAO,OACP,gBACA,WACD;SAGH,WAAU,aAAa,YAAY,OAAO,OAAO,WAAW;;EAIhE,MAAM,YAAY,cAAc,SAAS,WAAW;AAEpD,MAAI,UAAU,WAAW,EACvB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,gCAAgC,OAAO,MAAM,WAAW,KAAK;GACpE,CACF,EACF;EAGH,MAAM,YAAY,UACf,KAAK,GAAG,MAAM;GACb,MAAM,SAAS,IAAI,IAAI,EAAE,IAAI,EAAE,eAAe,WAAW,EAAE,YAAY,KAAK,EAAE,KAAK,UAAU,EAAE,UAAU,GAAG,EAAE,QAAQ,UAAU,EAAE,MAAM,QAAQ,EAAE,CAAC,QAAQ,EAAE,KAAK,UAAU,EAAE;GAG9K,MAAM,MAAM,EAAE,QAAQ,MAAM;AAI5B,UAAO,GAAG,OAAO,IAHD,IAAI,SAAS,gBACzB,IAAI,MAAM,GAAG,cAAc,GAAG,QAC9B;IAEJ,CACD,KAAK,cAAc;AAEtB,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,SAAS,UAAU,OAAO,kBAAkB,OAAO,MAAM,WAAW,KAAK,QAAQ;GACxF,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,iBAAiB,OAAO,EAAE;IAAI,CAAC;GAC/D,SAAS;GACV;;;AAeL,SAAgB,cACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,YAAY,gBAAgB,YAAY,OAAO,QAAQ;AAC7D,MAAI,aAAa,KACf,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAGH,MAAM,UAAU,WACb,QAAQ,8CAA8C,CACtD,IAAI,UAAU;AAEjB,MAAI,CAAC,QACH,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAGH,MAAM,gBAAgB,OAAO;AAC7B,MAAI,cAAc,SAAS,KAAK,IAAI,WAAW,cAAc,CAC3D,QAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,iBAAiB,OAAO,KAAK;IACpC,CACF;GACD,SAAS;GACV;EAGH,MAAM,WAAW,KAAK,QAAQ,WAAW,cAAc;EACvD,MAAM,eAAe,QAAQ,SAAS;EACtC,MAAM,eAAe,QAAQ,QAAQ,UAAU;AAE/C,MACE,CAAC,aAAa,WAAW,eAAe,IAAI,IAC5C,iBAAiB,aAEjB,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,2BAA2B,OAAO;IAAQ,CACjE;GACD,SAAS;GACV;AAGH,MAAI,CAAC,WAAW,SAAS,CACvB,QAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,mBAAmB,cAAc,aAAa,OAAO,QAAQ;IACpE,CACF;GACD,SAAS;GACV;EAGH,MAAM,OAAO,SAAS,SAAS;AAC/B,MAAI,KAAK,OAAO,IAAI,OAAO,KACzB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,2BAA2B,KAAK,OAAO,OAAO,MAAM,QAAQ,EAAE,CAAC;GACtE,CACF,EACF;EAIH,MAAM,WADU,aAAa,UAAU,OAAO,CACrB,MAAM,KAAK;EAEpC,MAAM,YAAY,OAAO,QAAQ,KAAK;EACtC,MAAM,SACJ,OAAO,SAAS,OACZ,KAAK,IAAI,WAAW,OAAO,OAAO,SAAS,OAAO,GAClD,SAAS;EAGf,MAAM,OADgB,SAAS,MAAM,UAAU,OAAO,CAC3B,KAAK,KAAK;AAOrC,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,GALhC,OAAO,QAAQ,OACX,GAAG,OAAO,QAAQ,GAAG,cAAc,UAAU,WAAW,EAAE,GAAG,OAAO,MACpE,GAAG,OAAO,QAAQ,GAAG,cAAc,GAGG,MAAM;GAAQ,CAAC,EAC1D;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,eAAe,OAAO,EAAE;IAAI,CAAC;GAC7D,SAAS;GACV;;;AAYL,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,IAAI,UAA6B;AAEjC,MAAI,OAAO,MAAM;GACf,MAAM,YAAY,gBAAgB,YAAY,OAAO,KAAK;AAC1D,OAAI,aAAa,KACf,WAAU,WACP,QACC,mJACD,CACA,IAAI,UAAU;QAInB,WAAU,sBAAsB,YADpB,QAAQ,KAAK,CACuB;AAGlD,MAAI,CAAC,QAIH,QAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAJZ,OAAO,OACnB,sBAAsB,OAAO,SAC7B,wDAAwD,QAAQ,KAAK;IAE9B,CAAC;GAC1C,SAAS,CAAC,OAAO;GAClB;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,cAAc,YAAY,QAAQ;GAAE,CAAC,EACtE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;AAcL,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,aAAuB,EAAE;EAC/B,MAAM,cAAmC,EAAE;AAE3C,MAAI,OAAO,QAAQ;AACjB,cAAW,KAAK,eAAe;AAC/B,eAAY,KAAK,OAAO,OAAO;;AAGjC,MAAI,OAAO,KAAK;AACd,cAAW,KACT,uGACD;AACD,eAAY,KAAK,OAAO,IAAI;;EAG9B,MAAM,QACJ,WAAW,SAAS,IAAI,SAAS,WAAW,KAAK,QAAQ,KAAK;EAChE,MAAM,QAAQ,OAAO,SAAS;AAC9B,cAAY,KAAK,MAAM;EAEvB,MAAM,WAAW,WACd,QACC;;WAEG,MAAM;;kBAGV,CACA,IAAI,GAAG,YAAY;AAUtB,MAAI,SAAS,WAAW,EACtB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;GACP,CACF,EACF;EAGH,MAAM,QAAQ,SAAS,KACpB,MACC,GAAG,EAAE,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,UAAU,cAAc,IAAI,KAAK,EAAE,WAAW,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,GAC5G;AAED,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,OAAO,kBAAkB,MAAM,KAAK,KAAK;GAC5D,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;AAcL,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,YAAY,gBAAgB,YAAY,OAAO,QAAQ;AAC7D,MAAI,aAAa,KACf,QAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,sBAAsB,OAAO;IAAW,CAC/D;GACD,SAAS;GACV;EAGH,MAAM,aAAa,CAAC,iBAAiB;EACrC,MAAM,cAAmC,CAAC,UAAU;AAEpD,MAAI,OAAO,QAAQ;AACjB,cAAW,KAAK,aAAa;AAC7B,eAAY,KAAK,OAAO,OAAO;;EAGjC,MAAM,QAAQ,OAAO,SAAS;AAC9B,cAAY,KAAK,MAAM;EAEvB,MAAM,WAAW,WACd,QACC;;iBAES,WAAW,KAAK,QAAQ,CAAC;;kBAGnC,CACA,IAAI,GAAG,YAAY;AAQtB,MAAI,SAAS,WAAW,EACtB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,kCAAkC,OAAO;GAChD,CACF,EACF;EAGH,MAAM,QAAQ,SAAS,KACpB,MACC,IAAI,OAAO,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,MAAM,wBAAwB,EAAE,WACzG;AAED,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,OAAO,kBAAkB,OAAO,QAAQ,OAAO,MAAM,KAAK,OAAO;GACpF,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;AAYL,SAAgB,mBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,IAAI,IAAI,OAAO,MAAM;EAC3B,MAAM,WAAW,WACd,QACC;;;;;;mBAOD,CACA,IAAI,GAAG,GAAG,EAAE;AAUf,MAAI,SAAS,WAAW,EACtB,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,gCAAgC,OAAO,MAAM;GACpD,CACF,EACF;EAGH,MAAM,QAAQ,SAAS,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,EAAE,OAAO,KAAK,EAAE,YAAY;AAE7E,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,GAAG,SAAS,OAAO,kBAAkB,OAAO,MAAM,QAAQ,MAAM,KAAK,KAAK;GACjF,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,0BAA0B,OAAO,EAAE;IAAI,CAC9D;GACD,SAAS;GACV;;;AAYL,SAAgB,kBACd,YACA,QACY;AACZ,KAAI;EACF,MAAM,YAAY,cAAc,YAAY,OAAO,IAAI;AAEvD,MAAI,CAAC,UAEH,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,yCALG,OAAO,OAAO,QAAQ,KAAK,CAKkB;GACvD,CACF,EACF;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,oBAAoB,UAAU;GAAE,CAAC,EAClE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAC7D;GACD,SAAS;GACV;;;AAYL,eAAsB,kBACpB,YACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,YAAY,UAAU,aAAa,aAAa,MAAM,OAC5D;EAEF,MAAM,EACJ,MAAM,UACN,UAAU,iBACR,MAAM,OAAO;EACjB,MAAM,EAAE,YAAY,MAAM,OAAO;EACjC,MAAM,EAAE,WAAW,QAAQ,MAAM,OAAO;EAaxC,MAAM,OAAO,WACV,QACC;;;8BAID,CACA,KAAK;EAER,MAAM,OAAO,SAAS;EACtB,MAAM,iBAAiB,SAAS,MAAM,WAAW,WAAW;EAE5D,SAAS,aAAa,UAAsC;GAC1D,MAAM,OAAO,aAAa,SAAS;AAOnC,UANmB;IACjB,SAAS,MAAM,OAAO,KAAK;IAC3B,SAAS,MAAM,OAAO,MAAM,KAAK;IACjC,SAAS,MAAM,WAAW,KAAK;IAC/B,SAAS,MAAM,YAAY,KAAK;IACjC,CACiB,MAAM,MAAM,SAAS,EAAE,CAAC;;EAG5C,SAAS,eAAe,YAA6B;AACnD,OAAI,CAAC,SAAS,eAAe,CAAE,QAAO;AACtC,OAAI;AACF,SAAK,MAAM,SAAS,YAAY,eAAe,EAAE;AAC/C,SAAI,UAAU,cAAc,CAAC,MAAM,WAAW,WAAW,CAAE;KAC3D,MAAM,OAAO,SAAS,gBAAgB,MAAM;AAC5C,SAAI;AACF,UAAI,CAAC,SAAS,KAAK,CAAC,aAAa,CAAE;aAC7B;AACN;;AAEF,SAAI,SAAS,SAAS,MAAM,QAAQ,CAAC,CAAE,QAAO;;WAE1C;AAGR,UAAO;;EAoBT,SAAS,mBAAmB,UAI1B;AAOA,QAAK,MAAM,OANE;IACX;IACA;IACA;IACA;IACD,EACuB;IACtB,MAAM,OAAO,SAAS,UAAU,IAAI;AACpC,QAAI,SAAS,KAAK,CAChB,KAAI;KACF,MAAM,MAAM,aAAa,MAAM,OAAO;AAEtC,YAAO;MAAE,OAAO;MAAM,MAAM;MAAK,cADb,iBAAiB,KAAK,IAAI;MACc;YACtD;AACN,YAAO;MAAE,OAAO;MAAM,MAAM;MAAK,cAAc;MAAO;;;AAI5D,UAAO;IAAE,OAAO;IAAO,MAAM;IAAM,cAAc;IAAO;;EAG1D,MAAM,UAA0B,KAAK,KAAK,MAAM;GAC9C,MAAM,aAAa,SAAS,EAAE,UAAU;GACxC,IAAI;GACJ,IAAI,gBAA+B;AAEnC,OAAI,WACF,UAAS;QACJ;AACL,oBAAgB,aAAa,EAAE,UAAU,IAAI;AAC7C,aAAS,gBAAgB,UAAU;;GAGrC,MAAM,OAAO,aACT,mBAAmB,EAAE,UAAU,GAC/B;IAAE,OAAO;IAAO,MAAM;IAAM,cAAc;IAAO;AAErD,UAAO;IACL,MAAM,EAAE;IACR,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,QAAQ,EAAE;IACV,MAAM,EAAE;IACR,eAAe,EAAE;IACjB;IACA,gBAAgB;IAChB,kBAAkB,eAAe,EAAE,YAAY;IAC/C;IACD;IACD;EAEF,MAAM,WACJ,CAAC,OAAO,YAAY,OAAO,aAAa,QACpC,UACA,QAAQ,QAAQ,MAAM,EAAE,WAAW,OAAO,SAAS;EAEzD,MAAM,UAAU;GACd,OAAO,KAAK;GACZ,QAAQ,QAAQ,QAAQ,MAAM,EAAE,WAAW,SAAS,CAAC;GACrD,OAAO,QAAQ,QAAQ,MAAM,EAAE,WAAW,QAAQ,CAAC;GACnD,MAAM,QAAQ,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC;GAClD;AAED,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM,KAAK,UAAU;IAAE;IAAS,UAAU;IAAU,EAAE,MAAM,EAAE;GAC/D,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAC7D;GACD,SAAS;GACV;;;;;;;AAgBL,MAAM,iBAAiB;CACrB;EAAE,KAAK;EAAuB,OAAO;EAAiB;CACtD;EAAE,KAAK;EAAyB,OAAO;EAAyB;CAChE;EAAE,KAAK;EAAuB,OAAO;EAAiB;CACtD;EAAE,KAAK;EAAuB,OAAO;EAAW;CACjD;;;;;;;;AASD,SAAS,iBAAiB,KAIxB;CACA,MAAM,QAAQ,IAAI,MAAM,KAAK;CAG7B,MAAM,cAAc,MAAM,WACvB,MAAM,EAAE,MAAM,KAAK,cACrB;AAED,KAAI,gBAAgB,GAClB,QAAO;EAAE,iBAAiB;EAAM,aAAa;EAAK,aAAa;EAAO;CAKxE,IAAI,SAAS,MAAM;AACnB,MAAK,IAAI,IAAI,cAAc,GAAG,IAAI,MAAM,QAAQ,KAAK;EACnD,MAAM,UAAU,MAAM,GAAG,MAAM;AAC/B,MAAI,YAAY,SAAU,QAAQ,WAAW,KAAK,IAAI,YAAY,eAAgB;AAChF,YAAS;AACT;;;AAOJ,QAAO;EAAE,iBAHa,MAAM,MAAM,aAAa,OAAO,CAChB,KAAK,KAAK,CAAC,MAAM;EAE7B,aAAa;EAAK,aAAa;EAAM;;AAGjE,SAAgB,gBACd,YACA,QACY;AACZ,KAAI;EACF,IAAI;EACJ,IAAI;AAEJ,MAAI,OAAO,SAAS;GAClB,MAAM,YAAY,gBAAgB,YAAY,OAAO,QAAQ;AAC7D,OAAI,aAAa,KACf,QAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAQ,MAAM,sBAAsB,OAAO;KAAW,CAC/D;IACD,SAAS;IACV;GAGH,MAAM,MAAM,WACT,QAAQ,oDAAoD,CAC5D,IAAI,UAAU;AAEjB,OAAI,CAAC,IACH,QAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAQ,MAAM,sBAAsB,OAAO;KAAW,CAC/D;IACD,SAAS;IACV;AAGH,cAAW,IAAI;AACf,iBAAc,IAAI;SACb;GAEL,MAAM,UAAU,sBAAsB,YAAY,QAAQ,KAAK,CAAC;AAChE,OAAI,CAAC,QACH,QAAO,EACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,wDAAwD,QAAQ,KAAK,CAAC;IAC7E,CACF,EACF;AAEH,cAAW,QAAQ;AACnB,iBAAc,QAAQ;;AAIxB,OAAK,MAAM,OAAO,gBAAgB;GAChC,MAAM,WAAW,KAAK,UAAU,IAAI,IAAI;AACxC,OAAI,WAAW,SAAS,EAAE;IAExB,MAAM,EAAE,iBAAiB,aAAa,gBAAgB,iBAD1C,aAAa,UAAU,OAAO,CACiC;IAE3E,IAAI;AACJ,QAAI,eAAe,gBAEjB,UAAS;KACP,eAAe,YAAY,GAAG,IAAI;KAClC;KACA;KACA;KACA;KACA;KACA;KACD,CAAC,KAAK,KAAK;QAEZ,UAAS;KACP,eAAe,YAAY,GAAG,IAAI;KAClC;KACA;KACD,CAAC,KAAK,KAAK;AAGd,WAAO,EACL,SAAS,CAAC;KAAE,MAAM;KAAQ,MAAM;KAAQ,CAAC,EAC1C;;;EAKL,MAAM,WAAW,eAAe,KAAK,MAAM,KAAK,SAAS,GAAG,EAAE,MAAM,CAAC,KAAK,KAAK;AAC/E,SAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;IACJ,iCAAiC;IACjC;IACA;IACA;IACA;IACA;IACD,CAAC,KAAK,KAAK;GACb,CACF,EACF;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;;;;;AA6BL,eAAsB,uBACpB,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,cAAc,MAAM,OAAO;EACnC,MAAM,SAAS,IAAI,WAAW;AAE9B,MAAI,OAAO,WAAW,OAAO;GAC3B,MAAM,EAAE,QAAQ,mBAAmB,MAAM,OAAO,uBAAuB;AAgBvE,UAAO,EACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAhBd;KACZ,SAAS,OAAO;KAChB,oBAAoB,eAAe,KAAK,KAAK,IAAI;KACjD;KACA;KACA,GAAG,OAAO,QAAQ,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,SAAS;AAEpD,aAAO,KAAK,GAAG,IADL,IACW,UAAU,YAAY;OAC3C;KACF;KACA;KACA,GAAG,OAAO,QAAQ,OAAO,QAAQ,CAAC,KAC/B,CAAC,OAAO,cAAc,KAAK,MAAM,IAAK,SAAsB,KAAK,KAAK,IAAI,WAC5E;KACF,CAEuC,KAAK,KAAK;IAAE,CAAC,EACpD;;AAGH,MAAI,OAAO,WAAW,OAAO;AAC3B,OAAI,CAAC,OAAO,QAAQ,CAAC,OAAO,YAAY,CAAC,OAAO,QAC9C,QAAO;IACL,SAAS,CACP;KACE,MAAM;KACN,MAAM;KACP,CACF;IACD,SAAS;IACV;AAOH,UAAO,EACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,uCATG,MAAM,OAAO,sBAAsB;KAChD,MAAM,OAAO;KACb,UAAU,OAAO;KACjB,SAAS,OAAO;KACjB,CAAC,EAKuD,OAAO;IAC3D,CACF,EACF;;AAGH,MAAI,OAAO,WAAW,QAAQ;AAC5B,OAAI,CAAC,OAAO,QACV,QAAO;IACL,SAAS,CACP;KAAE,MAAM;KAAQ,MAAM;KAAkD,CACzE;IACD,SAAS;IACV;GAEH,MAAM,SAAS,MAAM,OAAO,iBAAiB;IAC3C,OAAO,OAAO,SAAS;IACvB,SAAS,OAAO;IAChB,OAAO,OAAO;IACf,CAAC;AASF,UAAO,EACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MATd;KACZ,SAAS,OAAO;KAChB,cAAc,OAAO,kBAAkB,KAAK,KAAK,IAAI;KACrD,cAAc,OAAO,kBAAkB,KAAK,KAAK,IAAI;KACrD,GAAI,OAAO,eAAe,SAAS,IAC/B,CAAC,WAAW,OAAO,eAAe,KAAK,KAAK,GAAG,GAC/C,EAAE;KACP,CAEuC,KAAK,KAAK;IAAE,CAAC,EACpD;;AAGH,SAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM,mBAAmB,OAAO,OAAO,OAAO,CAAC;IAChD,CACF;GACD,SAAS;GACV;UACM,GAAG;AACV,SAAO;GACL,SAAS,CACP;IAAE,MAAM;IAAQ,MAAM,8BAA8B,OAAO,EAAE;IAAI,CAClE;GACD,SAAS;GACV;;;;;;;;;;;AA4BL,eAAsB,gBACpB,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,cAAc,MAAM,OAAO;EAGnC,MAAM,SAAS,MAFA,IAAI,WAAW,CAEF,WAAW;GACrC,SAAS,OAAO;GAChB,gBAAgB,OAAO;GACvB,WAAW,OAAO;GACnB,CAAC;EAEF,MAAM,QAAkB;GACtB,YAAY,OAAO;GACnB,oBAAoB,OAAO,kBAAkB;GAC7C,sBAAsB,OAAO,oBAAoB;GACjD,eAAe,OAAO,WAAW,QAAQ,EAAE;GAC3C,kBAAkB,OAAO;GAC1B;AAED,MAAI,OAAO,YAAY,SAAS,GAAG;AACjC,SAAM,KAAK,GAAG;AACd,SAAM,KAAK,eAAe;AAC1B,QAAK,MAAM,KAAK,OAAO,YACrB,OAAM,KAAK,KAAK,EAAE,KAAK,KAAK,EAAE,QAAQ,KAAK,QAAQ,EAAE,CAAC,GAAG;;AAI7D,MAAI,OAAO,SAAS;AAClB,SAAM,KAAK,GAAG;AACd,SAAM,KACJ,2DAA2D,OAAO,iBAAiB,kBAClE,OAAO,aAAa,KAAK,QAAQ,EAAE,CAAC,WAAW,OAAO,eAAe,IACvF;;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,MAAM,KAAK,KAAK;GAAE,CAAC,EACpD;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,uBAAuB,OAAO,EAAE;IAAI,CAAC;GACrE,SAAS;GACV;;;;;;;;;;;;;;AA0BL,eAAsB,iBACpB,YACA,YACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,WAAW,wBAAwB,MAAM,OAAO;EAExD,MAAM,SAAS,MAAM,UACnB,YACA,YACA,OAAO,KACP,OAAO,QACR;AAED,MAAI,CAAC,OAEH,QAAO,EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;IACJ,+BANO,OAAO,OAAO,QAAQ,KAAK;IAOlC;IACA,2CACG,OAAO,UAAU,sBAAsB;IAC1C;IACA;IACA;IACD,CAAC,KAAK,KAAK;GACb,CACF,EACF;AAGH,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,oBAAoB,OAAO;GAAE,CAAC,EAC/D;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,wBAAwB,OAAO,EAAE;IAAI,CAAC;GACtE,SAAS;GACV;;;AAeL,eAAsB,kBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,SAAS,cAAc,cAAc;GACzC,WAAW,OAAO;GAClB,OAAO,OAAO;GACd,WAAW,OAAO;GAClB,MAAM,OAAO;GACd,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAAC;GACvE,SAAS;GACV;;;AAeL,eAAsB,iBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EACtC,MAAM,SAAS,aAAa,cAAc;GACxC,OAAO,OAAO;GACd,aAAa,OAAO;GACpB,YAAY,OAAO;GACnB,SAAS,OAAO;GACjB,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,wBAAwB,OAAO,EAAE;IAAI,CAAC;GACtE,SAAS;GACV;;;AAgBL,eAAsB,mBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,mBAAmB,MAAM,OAAO;EACxC,MAAM,UAAU,MAAM,eAAe,cAAc;GACjD,eAAe,OAAO;GACtB,gBAAgB,OAAO;GACvB,OAAO,OAAO;GACd,eAAe,OAAO;GACtB,kBAAkB,OAAO;GAC1B,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;GAAE,CAAC,EACpE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,0BAA0B,OAAO,EAAE;IAAI,CAAC;GACxE,SAAS;GACV;;;AAeL,eAAsB,kBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EACvC,MAAM,UAAU,MAAM,cAAc,cAAc;GAChD,UAAU,OAAO;GACjB,gBAAgB,OAAO;GACvB,OAAO,OAAO;GACd,eAAe,OAAO;GACvB,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE;GAAE,CAAC,EACpE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,yBAAyB,OAAO,EAAE;IAAI,CAAC;GACvE,SAAS;GACV;;;AAeL,eAAsB,mBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,mBAAmB,MAAM,OAAO;EACxC,MAAM,SAAS,MAAM,eAAe,cAAc;GAChD,UAAU,OAAO;GACjB,gBAAgB,OAAO;GACvB,OAAO,OAAO;GACd,OAAO,OAAO;GACf,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,0BAA0B,OAAO,EAAE;IAAI,CAAC;GACxE,SAAS;GACV;;;AAgBL,eAAsB,iBACpB,cACA,QACqB;AACrB,KAAI;EACF,MAAM,EAAE,iBAAiB,MAAM,OAAO;EACtC,MAAM,SAAS,MAAM,aAAa,cAAc;GAC9C,gBAAgB,OAAO;GACvB,cAAc,OAAO;GACrB,gBAAgB,OAAO;GACvB,WAAW,OAAO;GAClB,qBAAqB,OAAO;GAC7B,CAAC;AACF,SAAO,EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAAM,KAAK,UAAU,QAAQ,MAAM,EAAE;GAAE,CAAC,EACnE;UACM,GAAG;AACV,SAAO;GACL,SAAS,CAAC;IAAE,MAAM;IAAQ,MAAM,wBAAwB,OAAO,EAAE;IAAI,CAAC;GACtE,SAAS;GACV;;;;;;;;AAeL,SAAS,qBACP,gBACA,iBACA,YACA,gBAAgB,IAChB,iBAAiB,IACD;AAChB,KAAI,eAAe,WAAW,KAAK,gBAAgB,WAAW,EAAG,QAAO,EAAE;CAE1E,MAAM,UAAU,MACd,GAAG,EAAE,UAAU,GAAG,EAAE,KAAK,GAAG,EAAE,UAAU,GAAG,EAAE;CAE/C,SAAS,gBAAgB,OAA4C;AACnE,MAAI,MAAM,WAAW,EAAG,wBAAO,IAAI,KAAK;EACxC,MAAM,MAAM,KAAK,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC;EAElD,MAAM,QADM,KAAK,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC,GAC9B;EACpB,MAAM,oBAAI,IAAI,KAAqB;AACnC,OAAK,MAAM,KAAK,MACd,GAAE,IAAI,OAAO,EAAE,EAAE,UAAU,IAAI,KAAK,EAAE,QAAQ,OAAO,MAAM;AAE7D,SAAO;;CAGT,MAAM,SAAS,gBAAgB,eAAe;CAC9C,MAAM,UAAU,gBAAgB,gBAAgB;CAEhD,MAAM,UAAU,IAAI,IAAY,CAC9B,GAAG,eAAe,IAAI,OAAO,EAC7B,GAAG,gBAAgB,IAAI,OAAO,CAC/B,CAAC;CAEF,MAAM,0BAAU,IAAI,KAA2B;AAC/C,MAAK,MAAM,KAAK,CAAC,GAAG,gBAAgB,GAAG,gBAAgB,CACrD,SAAQ,IAAI,OAAO,EAAE,EAAE,EAAE;CAG3B,MAAM,WAA4D,EAAE;AACpE,MAAK,MAAM,OAAO,SAAS;EACzB,MAAM,OAAO,QAAQ,IAAI,IAAI;EAC7B,MAAM,UAAU,OAAO,IAAI,IAAI,IAAI;EACnC,MAAM,WAAW,QAAQ,IAAI,IAAI,IAAI;EACrC,MAAM,gBAAgB,gBAAgB,UAAU,iBAAiB;AACjE,WAAS,KAAK;GAAE,GAAG;GAAM,OAAO;GAAe;GAAe,CAAC;;AAGjE,QAAO,SACJ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,CACjC,MAAM,GAAG,WAAW,CACpB,KAAK,EAAE,eAAe,SAAS,GAAG,QAAQ,EAAE"}
|