Initial commit
This commit is contained in:
433
skills/drafts-manager/SKILL.md
Normal file
433
skills/drafts-manager/SKILL.md
Normal file
@@ -0,0 +1,433 @@
|
||||
---
|
||||
name: drafts-manager
|
||||
description: Triage Drafts inbox and route notes to OmniFocus tasks or Obsidian documents
|
||||
triggers:
|
||||
- "triage drafts"
|
||||
- "process drafts"
|
||||
- "check drafts"
|
||||
- "what's in drafts"
|
||||
- "drafts inbox"
|
||||
- "clean up drafts"
|
||||
- "export drafts"
|
||||
allowed-tools: Read, Bash, Write
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Drafts Manager Skill
|
||||
|
||||
Triage the Drafts inbox using a hybrid AI + human confirmation workflow. Routes content to OmniFocus (tasks) or Obsidian (notes) based on content analysis.
|
||||
|
||||
## When to Activate
|
||||
|
||||
Use this skill when user wants to:
|
||||
- Triage or process their Drafts inbox
|
||||
- Route notes to OmniFocus or Obsidian
|
||||
- Check what's captured in Drafts
|
||||
- Clean up old drafts
|
||||
|
||||
## Architecture
|
||||
|
||||
**Two-Phase Hybrid Triage:**
|
||||
|
||||
1. **Export & Analyze** - Drafts exports inbox → Geoffrey analyzes → presents suggestions
|
||||
2. **Confirm & Process** - User confirms routing → Drafts processes each draft → archives
|
||||
|
||||
```
|
||||
┌─────────┐ URL ┌─────────┐ JSON ┌─────────┐ Table ┌──────┐
|
||||
│Geoffrey │ ────► │ Drafts │ ─────► │Geoffrey │ ──────► │ User │
|
||||
│ trigger │ │ export │ │ analyze │ │review│
|
||||
└─────────┘ └─────────┘ └─────────┘ └──┬───┘
|
||||
│
|
||||
Confirmed
|
||||
│
|
||||
┌─────────┐ URL ┌─────────┐ Routes ┌──────────┐ ┌───▼────┐
|
||||
│Geoffrey │ ────► │ Drafts │ ───────► │OmniFocus │ │Process │
|
||||
│ trigger │ │ process │ │ Obsidian │ │ list │
|
||||
└─────────┘ └────┬────┘ └──────────┘ └────────┘
|
||||
│
|
||||
Archives
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
Scripts are in `./scripts/` directory. Run via:
|
||||
```bash
|
||||
bun ./scripts/script-name.js
|
||||
```
|
||||
|
||||
### trigger_export.js
|
||||
|
||||
Triggers the Drafts "Geoffrey Export Inbox" action via URL scheme.
|
||||
|
||||
**Output:** Path to exported JSON file
|
||||
|
||||
**Use when:** Starting triage
|
||||
|
||||
### trigger_process.js
|
||||
|
||||
Triggers the Drafts "Geoffrey Process Draft" action with routing instructions.
|
||||
|
||||
**Parameters:** uuid, destination, project, tags, folder
|
||||
|
||||
**Use when:** Processing confirmed drafts
|
||||
|
||||
## Required Drafts Actions
|
||||
|
||||
**IMPORTANT:** User must install these Drafts actions (found in `./actions/`):
|
||||
|
||||
### Geoffrey Export Inbox
|
||||
|
||||
Exports all inbox drafts to a JSON file for Geoffrey to analyze.
|
||||
|
||||
**Location:** `~/Library/Mobile Documents/iCloud~com~agiletortoise~Drafts5/Documents/geoffrey-export.json`
|
||||
|
||||
**Output format:**
|
||||
```json
|
||||
{
|
||||
"exported": "2025-11-23T10:30:00Z",
|
||||
"count": 5,
|
||||
"drafts": [
|
||||
{
|
||||
"uuid": "ABC123",
|
||||
"title": "First line of draft",
|
||||
"content": "Full content...",
|
||||
"tags": ["inbox"],
|
||||
"createdAt": "2025-11-22T14:00:00Z",
|
||||
"modifiedAt": "2025-11-22T14:00:00Z",
|
||||
"isFlagged": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Geoffrey Process Draft
|
||||
|
||||
Processes a single draft based on routing instructions from URL parameters.
|
||||
|
||||
**URL Parameters:**
|
||||
- `uuid` - Draft to process
|
||||
- `destination` - "omnifocus", "obsidian", "archive", or "trash"
|
||||
- `project` - OmniFocus project (if destination=omnifocus)
|
||||
- `tags` - Comma-separated tags (if destination=omnifocus)
|
||||
- `dueDate` - Due date (if destination=omnifocus)
|
||||
- `folder` - Obsidian folder (if destination=obsidian)
|
||||
|
||||
## Content Analysis Rules
|
||||
|
||||
When analyzing drafts, look for these signals:
|
||||
|
||||
### Route to OmniFocus (Task)
|
||||
|
||||
**Signals:**
|
||||
- Starts with action verb: "call", "email", "buy", "schedule", "review", "check"
|
||||
- Contains: "todo", "task", "@due", "@defer"
|
||||
- Short (< 50 words)
|
||||
- Contains person names
|
||||
- Shopping lists or errands
|
||||
|
||||
**Apply omnifocus-manager routing rules for project/tag assignment**
|
||||
|
||||
### Route to Obsidian (Note)
|
||||
|
||||
**Signals:**
|
||||
- Longer content (> 100 words)
|
||||
- Meeting notes: "meeting with", "discussed", "attendees"
|
||||
- Ideas/brainstorms: "idea:", "thought:", "what if"
|
||||
- Reference material: links, quotes, research
|
||||
- Journal entries: "today I", feelings, reflections
|
||||
|
||||
### Archive in Drafts
|
||||
|
||||
**Signals:**
|
||||
- Reference that may be needed again
|
||||
- Snippets of code or text
|
||||
- Temporary notes that are now done
|
||||
|
||||
### Delete (Trash)
|
||||
|
||||
**Signals:**
|
||||
- Empty or nearly empty
|
||||
- Test/scratch content
|
||||
- Duplicates
|
||||
- Outdated info no longer needed
|
||||
|
||||
## Obsidian Routing Rules
|
||||
|
||||
| Content Type | Folder | Frontmatter |
|
||||
|--------------|--------|-------------|
|
||||
| Meeting notes | `Meetings/` | date, attendees, topics |
|
||||
| Ideas/brainstorms | `Geoffrey/Inbox/` | tags, created |
|
||||
| Research | `Reference/` | source, tags, related |
|
||||
| Journal | `Journal/` | date |
|
||||
| General notes | `Geoffrey/Inbox/` | tags, created |
|
||||
|
||||
**Frontmatter template:**
|
||||
```yaml
|
||||
---
|
||||
created: {{date}}
|
||||
source: drafts
|
||||
tags: [from-drafts]
|
||||
related: []
|
||||
---
|
||||
```
|
||||
|
||||
## Main Workflow: Triage Drafts
|
||||
|
||||
### Phase 1: Export & Analyze
|
||||
|
||||
1. **Trigger export:**
|
||||
```bash
|
||||
open "drafts://x-callback-url/runAction?action=Geoffrey%20Export%20Inbox"
|
||||
```
|
||||
|
||||
2. **Wait for export file** (2-3 seconds)
|
||||
|
||||
3. **Read exported JSON:**
|
||||
```bash
|
||||
cat ~/Library/Mobile\ Documents/iCloud~com~agiletortoise~Drafts5/Documents/geoffrey-export.json
|
||||
```
|
||||
|
||||
4. **Analyze each draft** using content signals above
|
||||
|
||||
5. **Present suggestions table:**
|
||||
```markdown
|
||||
## Drafts Inbox Triage
|
||||
|
||||
Found **5 drafts** to process:
|
||||
|
||||
| # | Title | Suggestion | Destination | Details |
|
||||
|---|-------|------------|-------------|---------|
|
||||
| 1 | Call John about... | Task | OmniFocus | Project: Meetings, Tags: John, Follow Up |
|
||||
| 2 | Meeting notes 11/22 | Note | Obsidian | Folder: Meetings/ |
|
||||
| 3 | [empty] | Delete | Trash | Empty draft |
|
||||
| 4 | Shopping list | Task | OmniFocus | Project: Single Actions, Tags: Chores |
|
||||
| 5 | Idea for app... | Note | Obsidian | Folder: Geoffrey/Inbox/ |
|
||||
|
||||
**Questions:**
|
||||
- #4: Should this go to a specific store location tag?
|
||||
|
||||
Which numbers need changes? (Or type "process all" to confirm)
|
||||
```
|
||||
|
||||
### Phase 2: Process & Archive
|
||||
|
||||
1. **For each confirmed draft**, trigger process action:
|
||||
```bash
|
||||
# OmniFocus task
|
||||
open "drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=ABC123&destination=omnifocus&project=Meetings&tags=John,Follow%20Up&dueDate=2025-11-30"
|
||||
|
||||
# Obsidian note
|
||||
open "drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=DEF456&destination=obsidian&folder=Meetings"
|
||||
|
||||
# Archive
|
||||
open "drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=GHI789&destination=archive"
|
||||
|
||||
# Delete
|
||||
open "drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=JKL012&destination=trash"
|
||||
```
|
||||
|
||||
2. **Report results:**
|
||||
```markdown
|
||||
## Summary
|
||||
Processed 5 drafts from inbox
|
||||
|
||||
## Actions
|
||||
- 2 tasks created in OmniFocus
|
||||
- 2 notes saved to Obsidian
|
||||
- 1 draft deleted
|
||||
|
||||
## Status
|
||||
✅ Complete
|
||||
|
||||
## Next Steps
|
||||
- Review tasks in OmniFocus inbox
|
||||
- Check Obsidian notes in Geoffrey/Inbox/
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Drafts not running:**
|
||||
```
|
||||
Status: ❌ Failed
|
||||
Error: Drafts app is not running. Please open Drafts and try again.
|
||||
```
|
||||
|
||||
**Action not installed:**
|
||||
```
|
||||
Status: ❌ Failed
|
||||
Error: Drafts action "Geoffrey Export Inbox" not found.
|
||||
Please install from: skills/drafts-manager/actions/
|
||||
```
|
||||
|
||||
**Export file not found:**
|
||||
```
|
||||
Status: ⚠️ Partial
|
||||
Error: Export file not created. Drafts may have timed out.
|
||||
Try running the action manually in Drafts.
|
||||
```
|
||||
|
||||
**OmniFocus not running:**
|
||||
```
|
||||
Status: ⚠️ Partial
|
||||
Warning: OmniFocus not running. Task creation may have failed.
|
||||
Please verify tasks were created.
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Drafts Actions
|
||||
|
||||
Import the action files from `./actions/`:
|
||||
|
||||
**Option A: Import from file**
|
||||
1. Open Drafts
|
||||
2. Go to Actions → Manage Actions
|
||||
3. Import from `actions/geoffrey-export-inbox.draftsAction`
|
||||
4. Import from `actions/geoffrey-process-draft.draftsAction`
|
||||
|
||||
**Option B: Create manually**
|
||||
1. Open Drafts
|
||||
2. Create new action "Geoffrey Export Inbox"
|
||||
3. Add Script step with code from `actions/geoffrey-export-inbox.js`
|
||||
4. Repeat for "Geoffrey Process Draft"
|
||||
|
||||
### 2. Verify Installation
|
||||
|
||||
Run: `open "drafts://x-callback-url/runAction?action=Geoffrey%20Export%20Inbox"`
|
||||
|
||||
Check for export file at:
|
||||
`~/Library/Mobile Documents/iCloud~com~agiletortoise~Drafts5/Documents/geoffrey-export.json`
|
||||
|
||||
## Tips for Best Results
|
||||
|
||||
### Tagging in Drafts
|
||||
|
||||
Use these tags for manual pre-routing:
|
||||
- `task` - Force route to OmniFocus
|
||||
- `note` - Force route to Obsidian
|
||||
- `archive` - Keep in Drafts archive
|
||||
- `delete` - Trash without review
|
||||
|
||||
### Quick Capture Patterns
|
||||
|
||||
When capturing to Drafts, these patterns help AI routing:
|
||||
- Start tasks with verbs: "Call", "Email", "Buy"
|
||||
- Start notes with context: "Meeting:", "Idea:", "Note:"
|
||||
- Use @ for OmniFocus hints: "@due(tomorrow)", "@project(Work)"
|
||||
|
||||
### Regular Triage
|
||||
|
||||
Best practice: Triage Drafts daily to keep inbox small
|
||||
- Morning: Process yesterday's captures
|
||||
- Evening: Quick review of day's notes
|
||||
|
||||
## Learned Routing Patterns
|
||||
|
||||
These patterns were learned through actual triage sessions with the user.
|
||||
|
||||
### Content Type → Destination
|
||||
|
||||
| Content Type | Destination | Obsidian Folder | OmniFocus Details |
|
||||
|--------------|-------------|-----------------|-------------------|
|
||||
| Meeting notes | Obsidian | `Meetings/2025/` | - |
|
||||
| Conference notes | Obsidian | `Meetings/2025/` | - |
|
||||
| Development roadmap | Obsidian | `Work/[Project]/Roadmap.md` | Format as checklist |
|
||||
| API keys/credentials | **Delete** | - | Security risk |
|
||||
| Error logs/JSON junk | **Delete** | - | Cruft |
|
||||
| Random passwords | **Delete** | - | Cruft |
|
||||
| Contact info | Contacts app | - | Also create follow-up task |
|
||||
| Book recommendations | OmniFocus | - | Tags: Reading |
|
||||
| Gift ideas | OmniFocus | - | Tags: Chores, Shopping |
|
||||
| Action items from meetings | OmniFocus | - | Tags: Follow Up |
|
||||
| Reference links (AI, UDL) | Obsidian | `Reference/` | - |
|
||||
| Personal project ideas | OmniFocus | - | Tags: Coding |
|
||||
| Presentation schedules | Obsidian | `Work/Presentations/` | Future dates → tasks |
|
||||
| Survey links | OmniFocus | - | Include links in notes |
|
||||
|
||||
### Signals for Each Destination
|
||||
|
||||
**→ OmniFocus Task:**
|
||||
- Starts with action verbs: "Submit", "Call", "Email", "Build", "Add"
|
||||
- Contains due date context: "first week of December", "by end of year"
|
||||
- Travel approvals, surveys to send, podcasts to record
|
||||
- Book to read, thing to buy, project to build
|
||||
- Multiple action items → single task with checklist in notes
|
||||
|
||||
**→ Obsidian Note:**
|
||||
- Meeting notes (look for date, attendees, discussion points)
|
||||
- Conference notes (look for session titles, speakers, links)
|
||||
- Development docs with code/commands (wrap in code blocks)
|
||||
- Reference material with links to external resources
|
||||
- Topic brainstorms and idea lists
|
||||
|
||||
**→ Contacts App:**
|
||||
- Contains name + phone/email
|
||||
- Create contact AND follow-up task if there's an action
|
||||
|
||||
**→ Delete:**
|
||||
- Empty or nearly empty
|
||||
- JSON/error logs
|
||||
- API keys (security)
|
||||
- Random strings/passwords
|
||||
- Test data that's no longer needed
|
||||
|
||||
**→ Archive in Drafts:**
|
||||
- Outdated but potentially useful later
|
||||
- Reference that may be needed again
|
||||
|
||||
### Project/Folder Mappings
|
||||
|
||||
**Obsidian Folders:**
|
||||
- `Meetings/2025/` - All meeting and conference notes
|
||||
- `Work/AI-Studio/` - AI Studio development docs
|
||||
- `Work/Jocular-Kangaroo/` - Jocular Kangaroo project
|
||||
- `Work/Presentations/` - Presentation archives
|
||||
- `Geoffrey/` - Geoffrey roadmap and ideas
|
||||
- `Reference/` - Reference materials and links
|
||||
|
||||
**OmniFocus Projects (go to Inbox if not found):**
|
||||
- CoSN Work - CoSN-related tasks
|
||||
- Meetings - Follow-ups with people
|
||||
- Research for Future Plans - Books, research tasks
|
||||
|
||||
**OmniFocus Tags:**
|
||||
- Follow Up - Tasks requiring follow-up
|
||||
- Reading - Books to read
|
||||
- Chores - Shopping, errands
|
||||
- Coding - Development tasks
|
||||
- Geoffrey - AI-assistable tasks
|
||||
- Email - Tasks requiring email
|
||||
|
||||
### Special Handling
|
||||
|
||||
**Multi-item notes:**
|
||||
- If note contains multiple distinct items, either:
|
||||
- Create single task with items as checklist in notes
|
||||
- Split into separate tasks (ask user preference)
|
||||
|
||||
**Development roadmaps:**
|
||||
- Format as Obsidian file with checkboxes
|
||||
- Use `- [ ]` for pending items
|
||||
- Use `- [x]` for completed items
|
||||
- Include code blocks for commands
|
||||
|
||||
**Sensitive data:**
|
||||
- API keys → Delete immediately
|
||||
- Student investigation data → Archive carefully, don't expose
|
||||
- Credentials → Delete or move to secure storage
|
||||
|
||||
**Contact + Task pattern:**
|
||||
- Add to Contacts app via add_contact.js
|
||||
- Create follow-up task with contact details in notes
|
||||
- Example: "Send maps to Kim Crowder" with email in notes
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Batch processing without confirmation for tagged drafts
|
||||
- [ ] Smart date extraction from draft content
|
||||
- [ ] Template matching for common note types
|
||||
- [ ] Sync with omnifocus-manager tag hierarchy
|
||||
- [ ] Obsidian template application
|
||||
- [ ] Draft content search/query
|
||||
- [ ] Auto-detect project roadmaps and format as checklists
|
||||
- [ ] Recognize CoSN, ACPE, Kiwanis contexts for project routing
|
||||
124
skills/drafts-manager/actions/README.md
Normal file
124
skills/drafts-manager/actions/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Installing Drafts Actions
|
||||
|
||||
These actions must be installed in Drafts before the drafts-manager skill can work.
|
||||
|
||||
## Quick Install
|
||||
|
||||
### Geoffrey Export Inbox
|
||||
|
||||
1. Open Drafts on your Mac
|
||||
2. Go to **Drafts → Settings → Actions → Manage Actions**
|
||||
3. Click the **+** button to create a new action
|
||||
4. Set the name to: `Geoffrey Export Inbox`
|
||||
5. Click **Steps** → Add **Script** step
|
||||
6. Copy the entire contents of `geoffrey-export-inbox.js` into the script editor
|
||||
7. Click **Done** to save
|
||||
|
||||
### Geoffrey Process Draft
|
||||
|
||||
1. Create another new action
|
||||
2. Set the name to: `Geoffrey Process Draft`
|
||||
3. Click **Steps** → Add **Script** step
|
||||
4. Copy the entire contents of `geoffrey-process-draft.js` into the script editor
|
||||
5. **Important:** Click the gear icon on the action and set:
|
||||
- After Success: **Do Nothing** (don't archive the draft - the script handles this)
|
||||
6. Click **Done** to save
|
||||
|
||||
## Verification
|
||||
|
||||
Test that the export action works:
|
||||
|
||||
```bash
|
||||
open "drafts://x-callback-url/runAction?action=Geoffrey%20Export%20Inbox"
|
||||
```
|
||||
|
||||
Check for the export file:
|
||||
|
||||
```bash
|
||||
ls -la ~/Library/Mobile\ Documents/iCloud~com~agiletortoise~Drafts5/Documents/geoffrey-export.json
|
||||
```
|
||||
|
||||
If the file exists and was recently modified, the action is working.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Action not found" error
|
||||
|
||||
- Make sure the action names match exactly:
|
||||
- `Geoffrey Export Inbox`
|
||||
- `Geoffrey Process Draft`
|
||||
- Check for extra spaces or different capitalization
|
||||
|
||||
### Export file not created
|
||||
|
||||
- Open Drafts manually and run the action from the action list
|
||||
- Check for script errors in Drafts' action log
|
||||
|
||||
### OmniFocus tasks not created
|
||||
|
||||
- Make sure OmniFocus is running
|
||||
- The OmniFocus URL scheme needs the app to be open
|
||||
|
||||
### Obsidian notes not created
|
||||
|
||||
- Make sure Obsidian is running
|
||||
- Verify the vault name is "Personal_Notes"
|
||||
- Check that the folder path exists
|
||||
|
||||
## Action Configuration
|
||||
|
||||
### Template Tags
|
||||
|
||||
The process action uses Drafts template tags to receive parameters. When called via URL:
|
||||
|
||||
```
|
||||
drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=ABC&destination=omnifocus
|
||||
```
|
||||
|
||||
The parameters become available as template tags in the script.
|
||||
|
||||
### Customization
|
||||
|
||||
To change the Obsidian vault path, edit this line in `geoffrey-process-draft.js`:
|
||||
|
||||
```javascript
|
||||
let obsidianVault = "/Users/hagelk/Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes";
|
||||
```
|
||||
|
||||
To change the default Obsidian folder, edit:
|
||||
|
||||
```javascript
|
||||
let folder = draft.getTemplateTag("folder") || "Geoffrey/Inbox";
|
||||
```
|
||||
|
||||
## URL Scheme Reference
|
||||
|
||||
### Export
|
||||
|
||||
```
|
||||
drafts://x-callback-url/runAction?action=Geoffrey%20Export%20Inbox
|
||||
```
|
||||
|
||||
### Process to OmniFocus
|
||||
|
||||
```
|
||||
drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=UUID&destination=omnifocus&project=Project&tags=Tag1,Tag2&dueDate=2025-11-30
|
||||
```
|
||||
|
||||
### Process to Obsidian
|
||||
|
||||
```
|
||||
drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=UUID&destination=obsidian&folder=Meetings
|
||||
```
|
||||
|
||||
### Archive
|
||||
|
||||
```
|
||||
drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=UUID&destination=archive
|
||||
```
|
||||
|
||||
### Trash
|
||||
|
||||
```
|
||||
drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&uuid=UUID&destination=trash
|
||||
```
|
||||
53
skills/drafts-manager/actions/geoffrey-export-inbox.js
Normal file
53
skills/drafts-manager/actions/geoffrey-export-inbox.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Geoffrey Export Inbox - Drafts Action
|
||||
// Exports all inbox drafts to JSON file for Geoffrey to analyze
|
||||
//
|
||||
// To install:
|
||||
// 1. Create new action in Drafts named "Geoffrey Export Inbox"
|
||||
// 2. Add a "Script" step
|
||||
// 3. Paste this code
|
||||
|
||||
// Query inbox drafts (not archived, not trashed)
|
||||
let drafts = Draft.query("", "inbox", [], [], "modified", true, false);
|
||||
|
||||
// Build export data
|
||||
let exportData = {
|
||||
exported: new Date().toISOString(),
|
||||
count: drafts.length,
|
||||
drafts: []
|
||||
};
|
||||
|
||||
// Process each draft
|
||||
drafts.forEach(d => {
|
||||
exportData.drafts.push({
|
||||
uuid: d.uuid,
|
||||
title: d.title || "(untitled)",
|
||||
content: d.content,
|
||||
tags: d.tags,
|
||||
createdAt: d.createdAt.toISOString(),
|
||||
modifiedAt: d.modifiedAt.toISOString(),
|
||||
isFlagged: d.isFlagged,
|
||||
wordCount: d.content.split(/\s+/).filter(w => w.length > 0).length
|
||||
});
|
||||
});
|
||||
|
||||
// Write to iCloud Documents folder
|
||||
let fm = FileManager.createCloud();
|
||||
let exportPath = "geoffrey-export.json";
|
||||
let jsonContent = JSON.stringify(exportData, null, 2);
|
||||
|
||||
let success = fm.writeString(exportPath, jsonContent);
|
||||
|
||||
if (success) {
|
||||
// Show confirmation
|
||||
app.displayInfoMessage("Exported " + drafts.length + " drafts for Geoffrey");
|
||||
|
||||
// Set draft content to summary (for callback if needed)
|
||||
draft.content = JSON.stringify({
|
||||
status: "success",
|
||||
count: drafts.length,
|
||||
path: exportPath
|
||||
});
|
||||
} else {
|
||||
app.displayErrorMessage("Failed to write export file");
|
||||
context.fail("Export failed");
|
||||
}
|
||||
185
skills/drafts-manager/actions/geoffrey-process-draft.js
Normal file
185
skills/drafts-manager/actions/geoffrey-process-draft.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// Geoffrey Process Draft - Drafts Action
|
||||
// Processes a single draft based on routing instructions
|
||||
//
|
||||
// Call via URL with JSON in text parameter:
|
||||
// drafts://x-callback-url/runAction?action=Geoffrey%20Process%20Draft&text={"uuid":"...","destination":"..."}
|
||||
//
|
||||
// JSON parameters:
|
||||
// - uuid: Draft UUID to process
|
||||
// - destination: "omnifocus", "obsidian", "archive", or "trash"
|
||||
// - project: OmniFocus project name (for omnifocus destination)
|
||||
// - tags: Comma-separated OmniFocus tags (for omnifocus destination)
|
||||
// - dueDate: Due date string (for omnifocus destination)
|
||||
// - folder: Obsidian folder path (for obsidian destination)
|
||||
//
|
||||
// To install:
|
||||
// 1. Create new action in Drafts named "Geoffrey Process Draft"
|
||||
// 2. Add a "Script" step
|
||||
// 3. Paste this code
|
||||
|
||||
// Parse parameters from draft content (passed as JSON via URL text parameter)
|
||||
let params;
|
||||
try {
|
||||
params = JSON.parse(draft.content);
|
||||
} catch (e) {
|
||||
app.displayErrorMessage("Invalid JSON parameters: " + e.message);
|
||||
context.fail("Invalid JSON");
|
||||
}
|
||||
|
||||
let uuid = params.uuid;
|
||||
let destination = params.destination;
|
||||
let project = params.project || "";
|
||||
let tags = params.tags || "";
|
||||
let dueDate = params.dueDate || "";
|
||||
let folder = params.folder || "Geoffrey/Inbox";
|
||||
|
||||
// Validate required parameters
|
||||
if (!uuid) {
|
||||
app.displayErrorMessage("Missing uuid parameter");
|
||||
context.fail("Missing uuid");
|
||||
}
|
||||
|
||||
if (!destination) {
|
||||
app.displayErrorMessage("Missing destination parameter");
|
||||
context.fail("Missing destination");
|
||||
}
|
||||
|
||||
// Find the draft to process
|
||||
let targetDraft = Draft.find(uuid);
|
||||
|
||||
if (!targetDraft) {
|
||||
app.displayErrorMessage("Draft not found: " + uuid);
|
||||
context.fail("Draft not found");
|
||||
}
|
||||
|
||||
let title = targetDraft.title || "(untitled)";
|
||||
let content = targetDraft.content;
|
||||
let result = { uuid: uuid, title: title, destination: destination };
|
||||
|
||||
// Route based on destination
|
||||
switch (destination) {
|
||||
case "omnifocus":
|
||||
// Build OmniFocus URL
|
||||
let ofUrl = "omnifocus:///add?";
|
||||
let params = [];
|
||||
|
||||
// Task name is first line
|
||||
params.push("name=" + encodeURIComponent(title));
|
||||
|
||||
// Note is rest of content
|
||||
let noteLines = content.split("\n").slice(1).join("\n").trim();
|
||||
if (noteLines) {
|
||||
params.push("note=" + encodeURIComponent(noteLines));
|
||||
}
|
||||
|
||||
// Project
|
||||
if (project) {
|
||||
params.push("project=" + encodeURIComponent(project));
|
||||
}
|
||||
|
||||
// Due date
|
||||
if (dueDate) {
|
||||
params.push("due=" + encodeURIComponent(dueDate));
|
||||
}
|
||||
|
||||
// Open OmniFocus URL
|
||||
let ofCallback = CallbackURL.create();
|
||||
ofCallback.baseURL = ofUrl + params.join("&");
|
||||
|
||||
let ofSuccess = ofCallback.open();
|
||||
|
||||
if (ofSuccess) {
|
||||
result.status = "success";
|
||||
result.project = project;
|
||||
result.tags = tags;
|
||||
|
||||
// Tag the draft as processed
|
||||
targetDraft.addTag("sent-to-omnifocus");
|
||||
targetDraft.isArchived = true;
|
||||
targetDraft.update();
|
||||
|
||||
app.displayInfoMessage("Task sent to OmniFocus: " + title);
|
||||
} else {
|
||||
result.status = "failed";
|
||||
result.error = "Failed to open OmniFocus";
|
||||
app.displayErrorMessage("Failed to send to OmniFocus");
|
||||
}
|
||||
break;
|
||||
|
||||
case "obsidian":
|
||||
// Build Obsidian file path
|
||||
let obsidianVault = "/Users/hagelk/Library/Mobile Documents/iCloud~md~obsidian/Documents/Personal_Notes";
|
||||
let fileName = title.replace(/[\/\\:*?"<>|]/g, "-") + ".md";
|
||||
let filePath = folder + "/" + fileName;
|
||||
|
||||
// Build frontmatter
|
||||
let frontmatter = [
|
||||
"---",
|
||||
"created: " + new Date().toISOString().split("T")[0],
|
||||
"source: drafts",
|
||||
"draft-uuid: " + uuid,
|
||||
"tags: [from-drafts]",
|
||||
"---",
|
||||
""
|
||||
].join("\n");
|
||||
|
||||
// Full content with frontmatter
|
||||
let obsidianContent = frontmatter + content;
|
||||
|
||||
// Use Obsidian URL scheme to create file
|
||||
let obsCallback = CallbackURL.create();
|
||||
obsCallback.baseURL = "obsidian://new";
|
||||
obsCallback.addParameter("vault", "Personal_Notes");
|
||||
obsCallback.addParameter("file", folder + "/" + fileName.replace(".md", ""));
|
||||
obsCallback.addParameter("content", obsidianContent);
|
||||
obsCallback.addParameter("overwrite", "true");
|
||||
|
||||
let obsSuccess = obsCallback.open();
|
||||
|
||||
if (obsSuccess) {
|
||||
result.status = "success";
|
||||
result.folder = folder;
|
||||
result.file = fileName;
|
||||
|
||||
// Tag the draft as processed
|
||||
targetDraft.addTag("sent-to-obsidian");
|
||||
targetDraft.isArchived = true;
|
||||
targetDraft.update();
|
||||
|
||||
app.displayInfoMessage("Note saved to Obsidian: " + fileName);
|
||||
} else {
|
||||
result.status = "failed";
|
||||
result.error = "Failed to create Obsidian note";
|
||||
app.displayErrorMessage("Failed to save to Obsidian");
|
||||
}
|
||||
break;
|
||||
|
||||
case "archive":
|
||||
// Simply archive the draft
|
||||
targetDraft.addTag("archived-by-geoffrey");
|
||||
targetDraft.isArchived = true;
|
||||
targetDraft.update();
|
||||
|
||||
result.status = "success";
|
||||
app.displayInfoMessage("Archived: " + title);
|
||||
break;
|
||||
|
||||
case "trash":
|
||||
// Move to trash
|
||||
targetDraft.addTag("trashed-by-geoffrey");
|
||||
targetDraft.isTrashed = true;
|
||||
targetDraft.update();
|
||||
|
||||
result.status = "success";
|
||||
app.displayInfoMessage("Trashed: " + title);
|
||||
break;
|
||||
|
||||
default:
|
||||
result.status = "failed";
|
||||
result.error = "Unknown destination: " + destination;
|
||||
app.displayErrorMessage("Unknown destination: " + destination);
|
||||
context.fail("Unknown destination");
|
||||
}
|
||||
|
||||
// Set draft content to result (for potential callback)
|
||||
draft.content = JSON.stringify(result, null, 2);
|
||||
94
skills/drafts-manager/scripts/add_contact.js
Normal file
94
skills/drafts-manager/scripts/add_contact.js
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env osascript -l JavaScript
|
||||
// Add contact to iCloud Contacts
|
||||
// Usage: osascript -l JavaScript add_contact.js '{"name": "First Last", "phone": "123-456-7890", "email": "email@example.com", "organization": "Company"}'
|
||||
//
|
||||
// All fields except name are optional
|
||||
|
||||
function run(argv) {
|
||||
if (argv.length === 0) {
|
||||
return JSON.stringify({
|
||||
error: "Missing parameters",
|
||||
usage: 'osascript -l JavaScript add_contact.js \'{"name":"...", "phone":"...", "email":"..."}\''
|
||||
});
|
||||
}
|
||||
|
||||
let params;
|
||||
try {
|
||||
params = JSON.parse(argv[0]);
|
||||
} catch (e) {
|
||||
return JSON.stringify({
|
||||
error: "Invalid JSON",
|
||||
details: e.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!params.name) {
|
||||
return JSON.stringify({ error: "Missing name parameter" });
|
||||
}
|
||||
|
||||
// Parse name into first/last
|
||||
let nameParts = params.name.trim().split(/\s+/);
|
||||
let firstName = nameParts[0] || "";
|
||||
let lastName = nameParts.slice(1).join(" ") || "";
|
||||
|
||||
const Contacts = Application("Contacts");
|
||||
|
||||
try {
|
||||
// Create new person
|
||||
let person = Contacts.Person({
|
||||
firstName: firstName,
|
||||
lastName: lastName
|
||||
});
|
||||
|
||||
// Add to contacts
|
||||
Contacts.people.push(person);
|
||||
|
||||
// Add phone if provided
|
||||
if (params.phone) {
|
||||
let phone = Contacts.Phone({
|
||||
label: "work",
|
||||
value: params.phone.replace(/[^\d+()-\s]/g, "")
|
||||
});
|
||||
person.phones.push(phone);
|
||||
}
|
||||
|
||||
// Add email if provided
|
||||
if (params.email) {
|
||||
let email = Contacts.Email({
|
||||
label: "work",
|
||||
value: params.email
|
||||
});
|
||||
person.emails.push(email);
|
||||
}
|
||||
|
||||
// Add organization if provided
|
||||
if (params.organization) {
|
||||
person.organization = params.organization;
|
||||
}
|
||||
|
||||
// Add note if provided
|
||||
if (params.note) {
|
||||
person.note = params.note;
|
||||
}
|
||||
|
||||
// Save
|
||||
Contacts.save();
|
||||
|
||||
return JSON.stringify({
|
||||
success: true,
|
||||
contact: {
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
phone: params.phone || null,
|
||||
email: params.email || null,
|
||||
organization: params.organization || null
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
return JSON.stringify({
|
||||
error: "Failed to create contact",
|
||||
details: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
85
skills/drafts-manager/scripts/trigger_export.js
Normal file
85
skills/drafts-manager/scripts/trigger_export.js
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bun
|
||||
// Trigger Geoffrey Export Inbox action in Drafts
|
||||
// Usage: bun trigger_export.js
|
||||
//
|
||||
// This triggers the Drafts action and waits for the export file to be created.
|
||||
// Returns the path to the export file.
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Export file location
|
||||
const exportPath = path.join(
|
||||
process.env.HOME,
|
||||
"Library/Mobile Documents/iCloud~com~agiletortoise~Drafts5/Documents/geoffrey-export.json"
|
||||
);
|
||||
|
||||
// Check if Drafts is running
|
||||
try {
|
||||
execSync('pgrep -x "Drafts"', { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Drafts is not running",
|
||||
suggestion: "Please open Drafts and try again"
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get current file modification time (if exists)
|
||||
let oldMtime = 0;
|
||||
try {
|
||||
const stats = fs.statSync(exportPath);
|
||||
oldMtime = stats.mtimeMs;
|
||||
} catch (e) {
|
||||
// File doesn't exist yet, that's fine
|
||||
}
|
||||
|
||||
// Trigger the export action
|
||||
const actionUrl = "drafts://x-callback-url/runAction?action=" +
|
||||
encodeURIComponent("Geoffrey Export Inbox");
|
||||
|
||||
try {
|
||||
execSync(`open "${actionUrl}"`, { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Failed to trigger Drafts action",
|
||||
details: e.message
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Wait for the export file to be updated (max 10 seconds)
|
||||
const maxWait = 10000;
|
||||
const checkInterval = 500;
|
||||
let waited = 0;
|
||||
|
||||
while (waited < maxWait) {
|
||||
try {
|
||||
const stats = fs.statSync(exportPath);
|
||||
if (stats.mtimeMs > oldMtime) {
|
||||
// File was updated, success!
|
||||
const data = JSON.parse(fs.readFileSync(exportPath, "utf-8"));
|
||||
console.log(JSON.stringify({
|
||||
status: "success",
|
||||
path: exportPath,
|
||||
count: data.count,
|
||||
exported: data.exported
|
||||
}));
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (e) {
|
||||
// File doesn't exist yet, keep waiting
|
||||
}
|
||||
|
||||
// Sleep
|
||||
execSync(`sleep ${checkInterval / 1000}`);
|
||||
waited += checkInterval;
|
||||
}
|
||||
|
||||
// Timeout
|
||||
console.error(JSON.stringify({
|
||||
error: "Timeout waiting for export file",
|
||||
suggestion: "Check if the 'Geoffrey Export Inbox' action is installed in Drafts"
|
||||
}));
|
||||
process.exit(1);
|
||||
94
skills/drafts-manager/scripts/trigger_process.js
Normal file
94
skills/drafts-manager/scripts/trigger_process.js
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bun
|
||||
// Trigger Geoffrey Process Draft action in Drafts
|
||||
// Usage: bun trigger_process.js '<json-params>'
|
||||
//
|
||||
// JSON params:
|
||||
// {
|
||||
// "uuid": "ABC123",
|
||||
// "destination": "omnifocus|obsidian|archive|trash",
|
||||
// "project": "Project Name", // for omnifocus
|
||||
// "tags": "Tag1,Tag2", // for omnifocus
|
||||
// "dueDate": "2025-11-30", // for omnifocus
|
||||
// "folder": "Geoffrey/Inbox" // for obsidian
|
||||
// }
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
|
||||
// Parse arguments
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Missing parameters",
|
||||
usage: 'bun trigger_process.js \'{"uuid":"...","destination":"..."}\''
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let params;
|
||||
try {
|
||||
params = JSON.parse(args[0]);
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Invalid JSON parameters",
|
||||
details: e.message
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!params.uuid) {
|
||||
console.error(JSON.stringify({ error: "Missing uuid parameter" }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!params.destination) {
|
||||
console.error(JSON.stringify({ error: "Missing destination parameter" }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const validDestinations = ["omnifocus", "obsidian", "archive", "trash"];
|
||||
if (!validDestinations.includes(params.destination)) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Invalid destination",
|
||||
valid: validDestinations
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if Drafts is running
|
||||
try {
|
||||
execSync('pgrep -x "Drafts"', { stdio: "pipe" });
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Drafts is not running",
|
||||
suggestion: "Please open Drafts and try again"
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build URL with JSON in text parameter
|
||||
const jsonPayload = JSON.stringify(params);
|
||||
const actionUrl = "drafts://x-callback-url/runAction?action=" +
|
||||
encodeURIComponent("Geoffrey Process Draft") +
|
||||
"&text=" + encodeURIComponent(jsonPayload);
|
||||
|
||||
// Trigger the action
|
||||
try {
|
||||
execSync(`open "${actionUrl}"`, { stdio: "pipe" });
|
||||
|
||||
// Give Drafts a moment to process
|
||||
execSync("sleep 1");
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: "success",
|
||||
uuid: params.uuid,
|
||||
destination: params.destination,
|
||||
details: params
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({
|
||||
error: "Failed to trigger Drafts action",
|
||||
details: e.message
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user