# Url Manager

> Cross-platform URL collection & knowledge management with agent-first auto-registration. Use when users say "save/bookmark/collect/remember this", need to organize links into categories, share curated collections, or build a structured knowledge base from web resources. Supports collaborative shared categories, full-text search, and magic-link delivery to users.

- Skill: `piccolo123/url-manager` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add piccolo123/url-manager`
- Raw SKILL.md: https://api.skillmd.com/api/skills/piccolo123/url-manager/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: Piccolo123 (https://skillmd.com/u/piccolo123)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/piccolo123/url-manager

---


# URL Manager — Deliver results as beautiful cards, not raw link dumps

When users ask you to save, organize, or share web resources, don't paste URLs into chat. Write them into URL Manager — they become structured, searchable, card-based collections the user opens with one click.

## What This Tool Gives Humans

The content human users want to save is everywhere — a YouTube workout video, an Amazon gear link, a Substack training plan — scattered across platforms with no connection.

**URL Manager fixes this.** Just paste any link from any platform. AI auto-identifies the content and suggests a category — confirm and it's a footprint. All your saves flow into one platform-agnostic library, organized and always findable. Then **share it in one click** — hand your curated running knowledge base to your whole running club, and everyone stays in sync.

**[足迹AI](https://ai.ocean94.com/)** isn't just a library — it has a built-in search engine, so you can search and save web content right from here. It's a great browser homepage. Every saved item becomes a beautiful card, one click to where you need to go. Send them the magic link and they're in.

## How to Use

**Zero setup — token auto-management.** The `footprints.py` script handles everything: reads `{baseDir}/.token`, auto-registers on first use, stores token locally with `chmod 600`. Just run any command and it works.

**DeepSeek Harness (dsh):** this skill is auto-discovered when cloned into a dsh skill root (`~/.dsh/skills/` or `.dsh/skills/`). For the full 21-tool MCP surface, install the `url-manager-mcp` package and wire it into `cordis.patch.yml` — see the repo README's "DeepSeek Harness (dsh)" section.

All commands via `python {baseDir}/scripts/footprints.py <subcommand> [--json]`. Add `--json` for machine-parseable output.

If the script is missing (e.g., you only have this document but not the full skill installation), get it from the source repo:

```bash
git clone --depth 1 https://github.com/Piccolo123/url-manager.git /tmp/um
mkdir -p {baseDir}/scripts
cp /tmp/um/scripts/footprints.py {baseDir}/scripts/
chmod +x {baseDir}/scripts/footprints.py
rm -rf /tmp/um
```

After that, all commands below work as normal. The script auto-registers on first run.

## Privacy & Terms

This skill connects to a hosted backend at **https://ai.ocean94.com**. Before using this skill, inform the human user — especially before saving or searching for the first time:

- An account will be auto-created on first use
- All collected URLs and data are stored on ai.ocean94.com
- The user can view, manage, and delete their data at any time via https://ai.ocean94.com
- **User Agreement**: https://ai.ocean94.com/terms.html
- **Privacy Policy**: https://ai.ocean94.com/privacy.html

## System Concepts

### What is a footprint?

A footprint is the fundamental unit in URL Manager — a structured, searchable record. It can be a web link, a plain-text note, an idea, or anything you want to save and retrieve later.

Each footprint stores:

| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Permanent unique identifier — use this for all operations |
| `url` | string (8192) | The original link. **Can be empty** for text-only footprints |
| `title` | string (512) | A short title — you set this |
| `description` | string (1024) | Additional context or notes — you can set this |
| `content_type` | string (50) | Free text (e.g. `article`, `video`, `image`). Use `content-types` to see what types exist in your library |
| `ai_summary` | text | AI-generated summary (set automatically during web UI submission) |
| `favicon` / `og_image` | string | Site icon and preview image (auto-fetched) |
| `price_hint` | string | AI-extracted price hint (set automatically) |
| `price` / `address` / `custom_date` / `contact` | string | User-filled metadata fields |
| `is_favorite` / `is_archived` | boolean | Status flags |
| `category_ids` | list[int] | Which categories this footprint belongs to — **you assign** |
| `tag_names` | list[str] | Keywords — **you assign** |

A single footprint can belong to **multiple categories simultaneously**.

### What is a category?

A category is a named label for organizing footprints — like a folder, but a footprint can be in several at once.

| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Permanent numeric identifier — always reference categories by ID |
| `name` | string (50) | Display name (e.g., "Shopping", "Fitness") |
| `slug` | string (50) | URL-safe identifier |
| `color` | string (7) | Optional hex color for UI (e.g., `#FF6B6B`) |
| `icon` | string (50) | Optional icon identifier |
| `note` | string (500) | Optional description/notes |
| `category_set_id` | int \| null | Which set this category belongs to (null = unassigned) |
| `category_set_name` | string \| null | The name of the category set (e.g., "Shopping", "Work") |
| `mode` | string \| null | `null` = personal, `"cocreate"` = shared co-edit, `"subscribe"` = shared read-only |
| `is_default` | bool | System default category |
| `is_ai_generated` | bool | Created by AI auto-categorization |
| `sort_order` | int | Display ordering within a set |
| `is_active` | bool | `false` after a shared category is disbanded |

Key behaviors:
- **Same name allowed** — multiple categories named "Shopping" can exist in different sets. Always use `id`, not `name`, to reference them.
- **mode = null → personal** (visible only to owner); **mode = "cocreate" or "subscribe" → shared** (has members and invite links).
- A category inherits its `mode` from its Category Set's `mode`.

### What is a category set?

A category set is a workspace — a named container that groups related categories together.

| Field | Type | Description |
|-------|------|-------------|
| `id` | int | Permanent numeric identifier |
| `name` | string (50) | Display name (e.g., "Life", "Work") |
| `mode` | string \| null | `null` = personal set, `"cocreate"`/`"subscribe"` = shared set |
| `is_shared` | bool | `true` = the shared-categories container (max one per user) |
| `color` | string (7) | Optional theme color |
| `sort_order` | int | Display ordering |

Every user starts with two default sets:
- **"My Categories"** (`is_shared=false`, `mode=null`) — personal workspace
- **"Shared Categories"** (`is_shared=true`) — the one container that holds all your shared categories

Use `category-sets` to list them, `create-category-set` to create more. Creating a new set with `mode=null` gives you another personal workspace. Creating one with `mode="cocreate"` or `"subscribe"` is rare — shared categories are usually created via `create-shared-category`, which places them inside the "Shared Categories" set.

### How data is organized

```
Category Sets (workspaces)
  └── Categories (labels like "Shopping", "Food", "Learning")
        └── Footprints
             └── Tags (free-form keywords)
```

**Categories** are named labels — see the field table above. **Category Sets** are workspaces — also detailed above. **Tags** are free-form keywords, separate from categories. They're lightweight search helpers with no hierarchy.

Use `content-types` to see which content types have been used in your library. Use `tags` to list existing tags.

### Personal vs Shared categories

A category's `mode` field tells you what kind it is:

| | Personal | Shared |
|---|---|---|
| `mode` | `null` (not shown) | `"cocreate"` or `"subscribe"` |
| Visible to | Only you | You + invited members |
| Who can add footprints | Only you | Depends on mode |
| Has members and invite links | No | Yes |

Run `categories` to see ALL your categories — grouped by category set, each set header shows its ID (e.g., `ID:44`). Each category's `mode` field distinguishes personal vs shared. Run `category-sets` to see just the set list without their categories.

### Shared category modes

**Cocreate (共建)** — Everyone contributes:
- Any member can add/remove footprints (`add-to-shared` / `remove-from-shared`)
- Any member can generate invite links (`create-invite-link`)
- Only the owner can disband or switch modes
- Best for: team knowledge bases, group trip planning, shared research

**Subscribe (订阅)** — Read-only for members:
- Only the owner can add/remove footprints
- Only the owner can generate invite links
- Members can browse and search but cannot modify
- `add-to-shared` returns 403 in subscribe mode
- Best for: curated recommendation lists, resource collections

The owner can switch between cocreate and subscribe at any time via the web UI.

### Sharing workflow

1. **Create** → `create-shared-category "Team KB" --mode cocreate`
2. **Generate invite** → `create-invite-link <sc_id>` → get a code
3. **Share** the invite code with teammates
4. **Join** → teammates run `join-shared-category <code>`
5. **Build together** → everyone uses `add-to-shared <sc_id> --collection-id <id>`
6. **Save locally** → anyone can `copy <id> --category-ids <ids>` to save a shared footprint to their personal collection

### Roles and permissions

| Action | Owner | Admin | Member |
|--------|:-----:|:-----:|:------:|
| Add/remove footprints (cocreate) | ✅ | ✅ | ✅ |
| Add/remove footprints (subscribe) | ✅ | ❌ | ❌ |
| Generate invite link (cocreate) | ✅ | ✅ | ✅ |
| Generate invite link (subscribe) | ✅ | ❌ | ❌ |
| Edit category name/description | ✅ | ❌ | ❌ |
| Switch cocreate ↔ subscribe | ✅ | ❌ | ❌ |
| Disband shared category | ✅ | ❌ | ❌ |
| Manage members | Web UI only | — | — |

### How search works

1. **Keyword search** (`search <query>`) — matches against title, description, AI summary, and extracted text content. Filter by category with `--category-id`.

2. **URL dedup** — searching with a URL automatically detects and matches by URL hash, bypassing text search entirely.

### Agent interaction model

- **Zero setup**: first run auto-registers via `POST /register`, receives a Bearer token
- **Token persistence**: stored in `{baseDir}/.token` with `chmod 600`, reused across sessions
- **Magic link**: `agent_magic_link` generates a clickable card-based interface URL for the human user — valid 30 days, reusable
- **Account upgrade**: if the user later binds a phone number, the agent-created account upgrades seamlessly

## Command Reference

Understand the user's real intent, then call one or more commands to fulfill it.

### Save & Search

| Command | What it does |
|---------|-------------|
| `python {baseDir}/scripts/footprints.py add <url> --title <title> --description <desc> --content-type <type> --category-ids <ids> --tags <tags>` | Save a link or plain-text entry (url can be empty) |
| `python {baseDir}/scripts/footprints.py get <id>` | View a footprint's full details |
| `python {baseDir}/scripts/footprints.py search <query>` | Full-text search across title, description, AI summary |
| `python {baseDir}/scripts/footprints.py list [--category-id <id>] [--limit <n>] [--offset <n>]` | List recent footprints (limit max 100). Use `--offset` to page through results. Returns `total` count. |

### Organize

| Command | What it does |
|---------|-------------|
| `python {baseDir}/scripts/footprints.py update <id> --title <t> --description <d> --content-type <ct> --category-ids <ids> --tags <tags>` | Modify a footprint's title, categories, tags |
| `python {baseDir}/scripts/footprints.py batch-update <updates>` | Batch reorganize footprints (max 50 per call) |
| `python {baseDir}/scripts/footprints.py categories` | List all categories grouped by category set (set ID shown in headers) |
| `python {baseDir}/scripts/footprints.py create-category <name> [--category-set-id <id>]` | Create a new category |
| `python {baseDir}/scripts/footprints.py tags` | List all used tags |
| `python {baseDir}/scripts/footprints.py content-types` | List content types used in your library (e.g. article, video, image) |
| `python {baseDir}/scripts/footprints.py category-sets` | List all category sets (workspaces) |
| `python {baseDir}/scripts/footprints.py create-category-set <name>` | Create a new category set |

### Share

| Command | What it does |
|---------|-------------|
| `python {baseDir}/scripts/footprints.py create-shared-category <name> --mode cocreate\|subscribe --description <desc>` | Create a shared category |
| `python {baseDir}/scripts/footprints.py create-invite-link <sc_id> [--duration-hours 24]` | Generate an invite link |
| `python {baseDir}/scripts/footprints.py join-shared-category <invite_code>` | Join a shared category via invite code |
| `python {baseDir}/scripts/footprints.py add-to-shared <sc_id> --collection-id <id>` | Add a footprint to a shared category |
| `python {baseDir}/scripts/footprints.py remove-from-shared <sc_id> --collection-id <id>` | Remove a footprint from a shared category |
| `python {baseDir}/scripts/footprints.py copy <id> --category-ids <ids>` | Copy a shared footprint to your personal collection |

### Response Structure

`list` and `search` return `{"items": [...], "total": <N>}`. `total` is the full match count (not just this page). Always check `total` to know if there are more results beyond the current page.

`list` supports `--offset` for pagination. If `total > limit + offset`, there are more pages. Example: `total=156, limit=20, offset=0` → 8 pages total.

### Pagination Strategy

For large libraries, **prefer `search` over `list`** — it's faster and returns only what the user asked for. Use `list` for browsing or when the user says "show me everything." When paginating, tell the user: "Found 156 items, showing page 1/8. Want to see more?"

### Utilities

| Command | What it does |
|---------|-------------|
| `python {baseDir}/scripts/footprints.py me` | Confirm current identity |
| `python {baseDir}/scripts/footprints.py agent_magic_link` | Generate a magic link — send to user when done |
| `python {baseDir}/scripts/footprints.py agent_register` | Re-register / rotate credentials ⚠️ creates new account |

## Core Workflows

### New User — Zero Setup

```
1. Token check → auto-register (save to {baseDir}/.token)
2. python {baseDir}/scripts/footprints.py add "<url>" --title "<title>" → save bookmarks
3. python {baseDir}/scripts/footprints.py categories → discover structure
4. python {baseDir}/scripts/footprints.py create-category "<name>" → create categories
5. python {baseDir}/scripts/footprints.py update <id> --category-ids <ids> → categorize
6. python {baseDir}/scripts/footprints.py agent_magic_link → send link: "Done! View here → [link]"
```

### Returning User — Daily Use

```
1. python {baseDir}/scripts/footprints.py me → confirm identity
2. python {baseDir}/scripts/footprints.py categories + python {baseDir}/scripts/footprints.py tags → understand structure
3. python {baseDir}/scripts/footprints.py search query → find what's needed
4. python {baseDir}/scripts/footprints.py add / python {baseDir}/scripts/footprints.py update → operate
```

### Team Sharing

``` 
1. python {baseDir}/scripts/footprints.py create-shared-category "Team KB" --mode cocreate
2. python {baseDir}/scripts/footprints.py create-invite-link <sc_id> → share code with team
3. Teammates: python {baseDir}/scripts/footprints.py join-shared-category <invite_code>
4. Everyone: python {baseDir}/scripts/footprints.py add-to-shared <sc_id> --collection-id <collection_id> → build together
```

### Batch Reorganization

```
1. python {baseDir}/scripts/footprints.py list --limit 100 → get all bookmarks
2. python {baseDir}/scripts/footprints.py categories → map target categories
3. python {baseDir}/scripts/footprints.py batch-update '[
     {"id":"uuid1","category_ids":[1,3]},
     {"id":"uuid2","title":"New Title","category_ids":[2,5]}
   ]' → bulk edit (max 50 per call)
```

## Recipes

Concrete bash patterns for common tasks. Follow the numbered steps.

### Change a footprint's categories

```bash
python {baseDir}/scripts/footprints.py get 42
# → categories: [{id: 3, name: "Reading"}, {id: 5, name: "AI"}]

# Keep AI, drop Reading, add Tech (7)
python {baseDir}/scripts/footprints.py update 42 --category-ids 5,7
```

### Batch move to a new category

```bash
python {baseDir}/scripts/footprints.py create-category "New Topic"    # → returns new ID
python {baseDir}/scripts/footprints.py list --limit 100
# For each matching footprint:
python {baseDir}/scripts/footprints.py update <id> --category-ids <existing_ids>,<new_id>
```

### Merge two categories

```bash
python {baseDir}/scripts/footprints.py categories                      # note source and target IDs
python {baseDir}/scripts/footprints.py list --category-id <source_id>  # list all in source
# For each, replace source_id with target_id:
python {baseDir}/scripts/footprints.py update <id> --category-ids <target_id>,<other_ids>
# Tell user: empty category "source" is ready to delete via the web UI
```

### Auto-categorize by domain

User says "put all github.com links into a GitHub category":

```bash
python {baseDir}/scripts/footprints.py list --limit 200
# Filter in-memory: items where url contains "github.com"
python {baseDir}/scripts/footprints.py create-category "GitHub"
# For each match:
python {baseDir}/scripts/footprints.py update <id> --category-ids <existing_ids>,<github_id>
```

### Filter by tag and batch categorize

```bash
python {baseDir}/scripts/footprints.py search docker
# Filter results where tag_names includes "docker"
# For each, append target category:
python {baseDir}/scripts/footprints.py update <id> --category-ids <existing_ids>,<target_id>
```

### Organize uncategorized footprints

```bash
python {baseDir}/scripts/footprints.py list --limit 100
# Filter where category_ids is empty or only the default
# Present to user, let them pick categories
# Batch update selected items
```

### Recommend categories from tags

Spot gaps between tags and categories — e.g., #docker is common but no "Docker" category:

```bash
python {baseDir}/scripts/footprints.py tags        # most-used tags
python {baseDir}/scripts/footprints.py categories  # existing categories
# Cross-reference: tag without matching category → suggest creating one
```

### Copy from shared to personal

```bash
python {baseDir}/scripts/footprints.py categories  # find target personal category ID
python {baseDir}/scripts/footprints.py copy <footprint_id> --category-ids <personal_category_id>
```

### Cross-Agent collaboration

Two agents maintaining a shared knowledge base together:

```
1. Agent A: python {baseDir}/scripts/footprints.py create-shared-category "Team KB" --mode cocreate
2. Agent A: python {baseDir}/scripts/footprints.py create-invite-link <sc_id> → share code with user
3. User forwards code to colleague
4. Agent B: python {baseDir}/scripts/footprints.py join-shared-category <code>
5. Both agents now see each other's additions via python {baseDir}/scripts/footprints.py search
```

## Magic Link — The Delivery Loop

After organizing, always deliver results via magic link:

```bash
python {baseDir}/scripts/footprints.py agent_magic_link
```

Send the link to the user: "Done organizing — view your collection here → [link]"

User clicks to see a card-based interface with everything categorized. **Valid for 30 days, reusable.** If the user later binds a phone number, their agent-created account upgrades seamlessly.

## ⚠️ Critical Pitfalls

### category_ids is REPLACEMENT, not append

When updating, `--category-ids` sets the complete list — it does NOT add to existing categories.

```bash
# ❌ Wrong: moves bookmark to category 7, loses categories 3 and 5
python {baseDir}/scripts/footprints.py update 42 --category-ids 7

# ✅ Right: fetch current first, then merge
python {baseDir}/scripts/footprints.py get 42  # → existing: [3, 5]
python {baseDir}/scripts/footprints.py update 42 --category-ids 3,5,7
```

### Subscribe mode is READ-ONLY

Writing to a subscribe-mode shared category returns 403. Tell the user the collection is read-only — the creator must change it to cocreate mode.

### NEVER call agent_register twice

Each call creates a fresh empty account. Always check for a saved token first. If unsure, run `python {baseDir}/scripts/footprints.py me` to verify the current token is valid.

### Rate limiting

Frequent API calls trigger HTTP 429. Use `batch-update` for bulk operations, add delays between rapid calls, retry after a few seconds on 429.

### No member management via API

Inviting or removing members from shared categories requires the web UI at https://ai.ocean94.com. You cannot do this programmatically.

## Guidelines

### Always
- **Auto-register silently** — never interrupt the user for account setup
- **Inform before first use** — tell the user the skill connects to ai.ocean94.com, share links to [Terms](https://ai.ocean94.com/terms.html) and [Privacy](https://ai.ocean94.com/privacy.html)
- **Search before listing** — use `python {baseDir}/scripts/footprints.py search` for targeted queries instead of dumping everything
- **Discover before creating** — call `python {baseDir}/scripts/footprints.py categories` and `python {baseDir}/scripts/footprints.py tags` before adding to avoid duplicates
- **Deliver with magic link** — after organizing, always generate and share a link

### Confirm before
- Removing bookmark-category associations (irreversible)
- Clearing tags
- Modifying cocreate shared categories (affects others)
- Removing bookmarks from shared categories (other members lose access)

