fleece-cli 0.3.1__tar.gz → 0.4.0__tar.gz
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.
- fleece_cli-0.4.0/.claude/skills/fleece-profile.md +81 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.github/workflows/publish.yml +3 -0
- fleece_cli-0.4.0/CLAUDE.md +123 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/PKG-INFO +1 -1
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/cli.py +94 -8
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/db.py +85 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/pyproject.toml +1 -1
- fleece_cli-0.3.1/CLAUDE.md +0 -49
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.agents/skills/fleece/SKILL.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-card.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-compare.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-credits.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-flights.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-hotels.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-mcc.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-news.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-partners.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-rates.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-recommend.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-roi.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.claude/skills/fleece-wallet.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.devcontainer/devcontainer.json +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.github/workflows/publish-skills.yml +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.gitignore +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/.streamlit/config.toml +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/Advertiser Disclosure.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/KnowledgeCatalog.json +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/LICENSE +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/MyCards.json +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/README.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/app.yaml +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/assets/default_card.png +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/assets/default_card.svg +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/docs/CNAME +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/docs/SEO.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/docs/index.html +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/docs/robots.txt +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/docs/sitemap.xml +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/fleece poc.ipynb +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/fleece.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/image_service.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/install.sh +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/mcc_codes.jsonl +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/migrate.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/pages/credit_cards.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/pages/my_credit_cards.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/plan.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/pointsyeah.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/prompts/agent_system_prompt.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/reference/Analyzer.md +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/requirements-test.txt +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/requirements.txt +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/style.css +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/test_fleece.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tests/__init__.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tests/test_brave_client.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tests/test_cli.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tests/test_pointsyeah.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tests/test_tools.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tools/__init__.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tools/brave_client.py +0 -0
- {fleece_cli-0.3.1 → fleece_cli-0.4.0}/tools/credit_card_tools.py +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fleece-profile
|
|
3
|
+
description: Manage the user's spending profile — monthly spend by category, annual fee tolerance, points programs, home airport, and redemption goal. Profile context is automatically injected into fleece wallet, fleece roi, and fleece recommend. Use when setting up or updating the user's preferences.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /fleece-profile
|
|
7
|
+
|
|
8
|
+
Manages the user's persistent spending profile stored in `fleece.db`. Once set, profile context is automatically injected into `fleece wallet`, `fleece roi`, and `fleece recommend` — no need to re-enter spend amounts each time.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
- User wants to set up their profile so research commands are personalised
|
|
12
|
+
- User updates their spending habits or preferences
|
|
13
|
+
- User asks why recommendations don't match their situation (profile may be empty or stale)
|
|
14
|
+
- Before running `fleece recommend` or `fleece roi` for the first time
|
|
15
|
+
|
|
16
|
+
## Profile fields
|
|
17
|
+
|
|
18
|
+
| Field | Description |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `dining_monthly` | Monthly dining spend ($) |
|
|
21
|
+
| `groceries_monthly` | Monthly groceries spend ($) |
|
|
22
|
+
| `travel_monthly` | Monthly travel spend ($) |
|
|
23
|
+
| `gas_monthly` | Monthly gas spend ($) |
|
|
24
|
+
| `other_monthly` | Monthly other spend ($) |
|
|
25
|
+
| `annual_fee_tolerance` | Max annual fee willing to pay ($) |
|
|
26
|
+
| `points_programs` | Preferred points programs (e.g. Amex MR, Chase UR) |
|
|
27
|
+
| `home_airport` | Home airport IATA code (e.g. JFK) |
|
|
28
|
+
| `goal` | Current redemption goal (e.g. business class to Tokyo) |
|
|
29
|
+
| `preferences` | Other preferences (e.g. no foreign transaction fees) |
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# See all available fields
|
|
35
|
+
fleece profile fields
|
|
36
|
+
|
|
37
|
+
# Show current profile
|
|
38
|
+
fleece profile show
|
|
39
|
+
fleece profile show --json
|
|
40
|
+
|
|
41
|
+
# Set fields
|
|
42
|
+
fleece profile set dining_monthly 600
|
|
43
|
+
fleece profile set travel_monthly 300
|
|
44
|
+
fleece profile set annual_fee_tolerance 550
|
|
45
|
+
fleece profile set points_programs "Amex MR, Chase UR"
|
|
46
|
+
fleece profile set home_airport JFK
|
|
47
|
+
fleece profile set goal "business class to Tokyo, June 2027"
|
|
48
|
+
|
|
49
|
+
# Clear a field
|
|
50
|
+
fleece profile unset goal
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## How profile enriches other commands
|
|
54
|
+
|
|
55
|
+
Once set, you don't need to pass spend amounts manually:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Without profile: must pass all spend flags
|
|
59
|
+
fleece roi "Amex Gold" --dining 600 --travel 300 --other 800
|
|
60
|
+
|
|
61
|
+
# With profile set: spend values pulled automatically
|
|
62
|
+
fleece roi "Amex Gold"
|
|
63
|
+
|
|
64
|
+
# fleece wallet and fleece recommend also use profile context automatically
|
|
65
|
+
fleece wallet
|
|
66
|
+
fleece recommend "travel rewards"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Setup workflow
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
fleece profile set dining_monthly 600
|
|
73
|
+
fleece profile set groceries_monthly 400
|
|
74
|
+
fleece profile set travel_monthly 300
|
|
75
|
+
fleece profile set gas_monthly 150
|
|
76
|
+
fleece profile set other_monthly 800
|
|
77
|
+
fleece profile set annual_fee_tolerance 250
|
|
78
|
+
fleece profile set home_airport JFK
|
|
79
|
+
fleece profile set goal "Fly business to Tokyo in 2027"
|
|
80
|
+
fleece profile show
|
|
81
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Fleece — Credit Card Research & Redemption
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**Fleece** is a credit card research and award redemption toolkit. Tagline: "Find the best card for deal saviors."
|
|
6
|
+
|
|
7
|
+
It has two surfaces:
|
|
8
|
+
1. **Streamlit chatbot** (`fleece.py`) — conversational AI assistant backed by OpenAI
|
|
9
|
+
2. **CLI** (`cli.py`) — 12 commands for card research, wallet analysis, MCC lookup, and award redemption
|
|
10
|
+
|
|
11
|
+
Published on PyPI as [`fleece-cli`](https://pypi.org/project/fleece-cli/) · current version **0.3.1**
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Tech Stack
|
|
16
|
+
|
|
17
|
+
| Layer | Technology |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Chatbot frontend | Streamlit |
|
|
20
|
+
| Chatbot LLM | OpenAI (gpt-3.5-turbo, gpt-4, gpt-4o) via LangChain |
|
|
21
|
+
| Chatbot memory | ConversationEntityMemory |
|
|
22
|
+
| CLI framework | Typer |
|
|
23
|
+
| Live research | Brave Search API |
|
|
24
|
+
| Card portfolio | SQLite (`fleece.db`) via `db.py` |
|
|
25
|
+
| MCC dataset | Bundled `mcc_codes.jsonl` (981 codes, offline) |
|
|
26
|
+
| Redemption URLs | `pointsyeah.py` (pure stdlib, no deps) |
|
|
27
|
+
| Language | Python 3.11+ |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## CLI Commands
|
|
32
|
+
|
|
33
|
+
### Research (requires `BRAVE_API_KEY`)
|
|
34
|
+
| Command | Description |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `fleece card "<name>"` | Full card report — fees, welcome offer, rates, credits, benefits |
|
|
37
|
+
| `fleece rates "<name>"` | Earning rates by spend category |
|
|
38
|
+
| `fleece partners "<name>"` | Transfer partners, ratios, timing |
|
|
39
|
+
| `fleece credits "<name>"` | Statement credits and perks |
|
|
40
|
+
| `fleece news "<name>"` | Recent changes (past month, freshness-filtered) |
|
|
41
|
+
| `fleece compare "<A>" "<B>"` | Side-by-side comparison |
|
|
42
|
+
| `fleece wallet` | Portfolio analysis — coverage, overlaps, gaps, next-card suggestions |
|
|
43
|
+
| `fleece roi "<name>"` | First-year ROI estimate by spend profile |
|
|
44
|
+
| `fleece recommend "<profile>"` | Card recommendations for a spending profile |
|
|
45
|
+
|
|
46
|
+
### Redemption (no API key needed — work fully offline)
|
|
47
|
+
| Command | Description |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `fleece mcc <code>` | Offline MCC code lookup (981 codes bundled). Add `--wallet` to cross-reference saved cards |
|
|
50
|
+
| `fleece flights <ORIGIN> <DEST> --date <YYYY-MM-DD>` | PointsYeah award flight search URL |
|
|
51
|
+
| `fleece hotels "<location>" --checkin <date> --checkout <date>` | PointsYeah award hotel search URL |
|
|
52
|
+
|
|
53
|
+
All commands support `--json` for agent-friendly output and `-` to read arguments from stdin.
|
|
54
|
+
|
|
55
|
+
### BRAVE_API_KEY
|
|
56
|
+
Optional at startup — checked only when a research command actually runs. `mcc`, `flights`, and `hotels` work with no key set.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Key Files
|
|
61
|
+
|
|
62
|
+
| File | Purpose |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `cli.py` | Main CLI entry point (Typer app) |
|
|
65
|
+
| `fleece.py` | Streamlit chatbot app |
|
|
66
|
+
| `db.py` | SQLite helpers for card portfolio (`fleece.db`) |
|
|
67
|
+
| `pointsyeah.py` | PointsYeah URL generation (pure stdlib, merged from archived `pointsyeah-cli`) |
|
|
68
|
+
| `mcc_codes.jsonl` | Bundled MCC dataset (981 codes, source: greggles/mcc-codes) |
|
|
69
|
+
| `tools/brave_client.py` | Brave Search API client |
|
|
70
|
+
| `tools/credit_card_tools.py` | LangChain tools for the chatbot |
|
|
71
|
+
| `pyproject.toml` | Package config — hatchling build, `fleece` entry point |
|
|
72
|
+
| `install.sh` | Installs Claude Code skills and/or agent skill |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Agent Skills
|
|
77
|
+
|
|
78
|
+
### Claude Code (`/.claude/skills/`)
|
|
79
|
+
12 slash commands installed via `bash install.sh --claude`:
|
|
80
|
+
|
|
81
|
+
**Research:** `/fleece-card` `/fleece-rates` `/fleece-partners` `/fleece-credits` `/fleece-news` `/fleece-compare` `/fleece-wallet` `/fleece-roi` `/fleece-recommend`
|
|
82
|
+
|
|
83
|
+
**Redemption:** `/fleece-mcc` `/fleece-flights` `/fleece-hotels`
|
|
84
|
+
|
|
85
|
+
### ClawHub / OpenClaw (`/.agents/skills/fleece/SKILL.md`)
|
|
86
|
+
Published on ClawHub as `fleece@1.3.0`. Install via `clawhub install fleece` or `bash install.sh --agents`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## CI/CD
|
|
91
|
+
|
|
92
|
+
| Workflow | Trigger | Action |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `publish.yml` | Push `v*` tag | Build and publish to PyPI via OIDC trusted publishing |
|
|
95
|
+
| `publish-skills.yml` | Push to `main` touching `.agents/skills/` or `.claude/skills/` | Publish to ClawHub via `CLAWHUB_TOKEN` secret |
|
|
96
|
+
|
|
97
|
+
GitHub environment `pypi` is required for the PyPI workflow (OIDC).
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Landing Page
|
|
102
|
+
|
|
103
|
+
`docs/` is served as GitHub Pages at **https://getfleece.io/**.
|
|
104
|
+
|
|
105
|
+
Contains `index.html`, `sitemap.xml`, `robots.txt`. Submitted to Google Search Console. JSON-LD structured data included.
|
|
106
|
+
|
|
107
|
+
SEO notes tracked in `docs/SEO.md`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Infrastructure Notes
|
|
112
|
+
|
|
113
|
+
- **Databricks**: No resources available. Do not suggest Databricks solutions until provisioned.
|
|
114
|
+
- **pointsyeah-cli**: Archived. All functionality merged into fleece (`pointsyeah.py`, `fleece flights`, `fleece hotels`).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Development Notes
|
|
119
|
+
|
|
120
|
+
- Author: Yuan Chen
|
|
121
|
+
- Created: March 16, 2025
|
|
122
|
+
- Chatbot uses custom CSS styling (`style.css`)
|
|
123
|
+
- OpenAI API key entered via Streamlit sidebar — not stored
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fleece-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Credit card research and redemption CLI — live data via Brave Search, PointsYeah URL generation, designed for humans and AI agents.
|
|
5
5
|
Author-email: Yuan Chen <cysbc1999@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -250,14 +250,18 @@ def wallet(
|
|
|
250
250
|
except Exception as e:
|
|
251
251
|
_error_exit(str(e), code=1)
|
|
252
252
|
|
|
253
|
+
import db as _db
|
|
254
|
+
profile_ctx = _db.profile_as_context()
|
|
255
|
+
|
|
253
256
|
combined = "\n\n".join(parts)
|
|
254
|
-
combined +=
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
combined += "\n\nBased on the above, identify:\n"
|
|
258
|
+
combined += "1. Category coverage map\n"
|
|
259
|
+
combined += "2. Overlapping benefits or redundant categories\n"
|
|
260
|
+
combined += "3. Gaps — categories with no bonus multiplier\n"
|
|
261
|
+
combined += "4. Top 1-2 cards that would complement this portfolio"
|
|
262
|
+
if profile_ctx:
|
|
263
|
+
combined += f"\n\nUser profile context: {profile_ctx}"
|
|
264
|
+
combined += "\nTailor the gap analysis and recommendations to this profile."
|
|
261
265
|
_emit(combined, as_json, "wallet", ", ".join(card_list))
|
|
262
266
|
|
|
263
267
|
|
|
@@ -274,10 +278,20 @@ def roi(
|
|
|
274
278
|
"""First-year ROI estimate — welcome bonus + earn + credits minus annual fee."""
|
|
275
279
|
from tools.brave_client import search_and_format
|
|
276
280
|
from tools.credit_card_tools import _guess_cpp
|
|
281
|
+
import db as _db
|
|
277
282
|
|
|
278
283
|
name = _read_stdin_or_arg(name)
|
|
279
284
|
key = _resolve_key(api_key, no_dotenv)
|
|
280
285
|
|
|
286
|
+
# Pull spend values from saved profile if not provided on the command line
|
|
287
|
+
p = _db.get_profile()
|
|
288
|
+
if travel == 0.0 and p.get("travel_monthly"):
|
|
289
|
+
travel = float(p["travel_monthly"])
|
|
290
|
+
if dining == 0.0 and p.get("dining_monthly"):
|
|
291
|
+
dining = float(p["dining_monthly"])
|
|
292
|
+
if other == 0.0 and p.get("other_monthly"):
|
|
293
|
+
other = float(p["other_monthly"])
|
|
294
|
+
|
|
281
295
|
annual_travel = travel * 12
|
|
282
296
|
annual_dining = dining * 12
|
|
283
297
|
annual_other = other * 12
|
|
@@ -310,10 +324,16 @@ def recommend(
|
|
|
310
324
|
no_dotenv: NoDotenv = False,
|
|
311
325
|
):
|
|
312
326
|
"""Card recommendations matched to a spending profile and preferences."""
|
|
327
|
+
import db as _db
|
|
313
328
|
profile = _read_stdin_or_arg(profile)
|
|
314
329
|
key = _resolve_key(api_key, no_dotenv)
|
|
315
330
|
pref = f"{preferences} " if preferences else ""
|
|
316
|
-
|
|
331
|
+
|
|
332
|
+
# Enrich with saved profile context if available
|
|
333
|
+
saved_ctx = _db.profile_as_context(cards=_db.get_card_names())
|
|
334
|
+
ctx = f"{saved_ctx} | " if saved_ctx else ""
|
|
335
|
+
|
|
336
|
+
query = f"best credit cards {ctx}{profile} {pref}US 2025 site:nerdwallet.com OR site:thepointsguy.com OR site:doctorofcredit.com"
|
|
317
337
|
_run_search(_get_wrapper(key), query, "recommend", as_json)
|
|
318
338
|
|
|
319
339
|
|
|
@@ -465,6 +485,72 @@ def hotels(
|
|
|
465
485
|
webbrowser.open(url)
|
|
466
486
|
|
|
467
487
|
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
# profile — spending profile management
|
|
490
|
+
# ---------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
profile_app = typer.Typer(name="profile", help="Manage your spending profile — used to personalise research commands.", no_args_is_help=True)
|
|
493
|
+
app.add_typer(profile_app)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@profile_app.command("show")
|
|
497
|
+
def profile_show(as_json: JsonOpt = False):
|
|
498
|
+
"""Show your current spending profile."""
|
|
499
|
+
import db as _db
|
|
500
|
+
p = _db.get_profile()
|
|
501
|
+
if not p:
|
|
502
|
+
typer.echo("No profile set. Run: fleece profile set <field> <value>")
|
|
503
|
+
typer.echo(f"Fields: {', '.join(_db.PROFILE_FIELDS)}")
|
|
504
|
+
return
|
|
505
|
+
if as_json:
|
|
506
|
+
typer.echo(json.dumps({"command": "profile show", "profile": p, "ok": True, "error": None}))
|
|
507
|
+
return
|
|
508
|
+
for key, label in _db.PROFILE_FIELDS.items():
|
|
509
|
+
val = p.get(key, "")
|
|
510
|
+
if val:
|
|
511
|
+
typer.echo(f" {label:<45} {val}")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@profile_app.command("set")
|
|
515
|
+
def profile_set(
|
|
516
|
+
field: Annotated[str, typer.Argument(help=f"Field name. One of: {', '.join(__import__('db').PROFILE_FIELDS)}")],
|
|
517
|
+
value: Annotated[str, typer.Argument(help="Value to set.")],
|
|
518
|
+
as_json: JsonOpt = False,
|
|
519
|
+
):
|
|
520
|
+
"""Set a profile field."""
|
|
521
|
+
import db as _db
|
|
522
|
+
try:
|
|
523
|
+
_db.set_profile_field(field, value)
|
|
524
|
+
except ValueError as e:
|
|
525
|
+
_error_exit(str(e), code=1)
|
|
526
|
+
if as_json:
|
|
527
|
+
typer.echo(json.dumps({"command": "profile set", "field": field, "value": value, "ok": True, "error": None}))
|
|
528
|
+
else:
|
|
529
|
+
typer.echo(f"Profile updated: {field} = {value}")
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@profile_app.command("unset")
|
|
533
|
+
def profile_unset(
|
|
534
|
+
field: Annotated[str, typer.Argument(help="Field name to clear.")],
|
|
535
|
+
as_json: JsonOpt = False,
|
|
536
|
+
):
|
|
537
|
+
"""Clear a single profile field."""
|
|
538
|
+
import db as _db
|
|
539
|
+
_db.clear_profile_field(field)
|
|
540
|
+
if as_json:
|
|
541
|
+
typer.echo(json.dumps({"command": "profile unset", "field": field, "ok": True, "error": None}))
|
|
542
|
+
else:
|
|
543
|
+
typer.echo(f"Cleared: {field}")
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@profile_app.command("fields")
|
|
547
|
+
def profile_fields():
|
|
548
|
+
"""List all available profile fields and their descriptions."""
|
|
549
|
+
import db as _db
|
|
550
|
+
for key, label in _db.PROFILE_FIELDS.items():
|
|
551
|
+
typer.echo(f" {key:<30} {label}")
|
|
552
|
+
|
|
553
|
+
|
|
468
554
|
# ---------------------------------------------------------------------------
|
|
469
555
|
# cards — profile management (read/write user_cards.json)
|
|
470
556
|
# ---------------------------------------------------------------------------
|
|
@@ -23,8 +23,26 @@ CREATE TABLE IF NOT EXISTS cards (
|
|
|
23
23
|
image_url TEXT NOT NULL DEFAULT '',
|
|
24
24
|
date_added TEXT NOT NULL DEFAULT (date('now'))
|
|
25
25
|
);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS profile (
|
|
27
|
+
key TEXT PRIMARY KEY,
|
|
28
|
+
value TEXT NOT NULL DEFAULT ''
|
|
29
|
+
);
|
|
26
30
|
"""
|
|
27
31
|
|
|
32
|
+
# Default profile fields and their human-readable labels
|
|
33
|
+
PROFILE_FIELDS = {
|
|
34
|
+
"dining_monthly": "Monthly dining spend ($)",
|
|
35
|
+
"groceries_monthly": "Monthly groceries spend ($)",
|
|
36
|
+
"travel_monthly": "Monthly travel spend ($)",
|
|
37
|
+
"gas_monthly": "Monthly gas spend ($)",
|
|
38
|
+
"other_monthly": "Monthly other spend ($)",
|
|
39
|
+
"annual_fee_tolerance": "Max annual fee willing to pay ($)",
|
|
40
|
+
"points_programs": "Preferred points programs (e.g. Amex MR, Chase UR)",
|
|
41
|
+
"home_airport": "Home airport IATA code (e.g. JFK)",
|
|
42
|
+
"goal": "Current redemption goal (e.g. business class to Tokyo)",
|
|
43
|
+
"preferences": "Other preferences (e.g. no foreign transaction fees)",
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
_COLUMNS = ("id", "name", "last_four", "annual_fee", "credit_limit",
|
|
29
47
|
"rewards", "expiration", "image_url", "date_added")
|
|
30
48
|
|
|
@@ -133,6 +151,73 @@ def remove_card(name: str) -> bool:
|
|
|
133
151
|
return cur.rowcount > 0
|
|
134
152
|
|
|
135
153
|
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Profile
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def get_profile() -> dict[str, str]:
|
|
159
|
+
"""Return the full profile as {key: value}. Missing keys return ''."""
|
|
160
|
+
init_db()
|
|
161
|
+
with _conn() as con:
|
|
162
|
+
rows = con.execute("SELECT key, value FROM profile").fetchall()
|
|
163
|
+
return {r["key"]: r["value"] for r in rows}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def set_profile_field(key: str, value: str) -> None:
|
|
167
|
+
"""Upsert a single profile field."""
|
|
168
|
+
if key not in PROFILE_FIELDS:
|
|
169
|
+
raise ValueError(f'Unknown profile field "{key}". Valid fields: {", ".join(PROFILE_FIELDS)}')
|
|
170
|
+
init_db()
|
|
171
|
+
with _conn() as con:
|
|
172
|
+
con.execute(
|
|
173
|
+
"INSERT INTO profile (key, value) VALUES (?, ?) "
|
|
174
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
175
|
+
(key, value),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def clear_profile_field(key: str) -> None:
|
|
180
|
+
"""Remove a single profile field."""
|
|
181
|
+
init_db()
|
|
182
|
+
with _conn() as con:
|
|
183
|
+
con.execute("DELETE FROM profile WHERE key = ?", (key,))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def profile_as_context(cards: list[str] | None = None) -> str:
|
|
187
|
+
"""
|
|
188
|
+
Return a compact natural-language summary of the user profile suitable
|
|
189
|
+
for injecting into a Brave Search query or LLM prompt.
|
|
190
|
+
"""
|
|
191
|
+
p = get_profile()
|
|
192
|
+
parts = []
|
|
193
|
+
|
|
194
|
+
spend_fields = [
|
|
195
|
+
("dining_monthly", "dining"),
|
|
196
|
+
("groceries_monthly", "groceries"),
|
|
197
|
+
("travel_monthly", "travel"),
|
|
198
|
+
("gas_monthly", "gas"),
|
|
199
|
+
("other_monthly", "other"),
|
|
200
|
+
]
|
|
201
|
+
spend_parts = [f"${p[k]}/mo {label}" for k, label in spend_fields if p.get(k)]
|
|
202
|
+
if spend_parts:
|
|
203
|
+
parts.append("Spending: " + ", ".join(spend_parts))
|
|
204
|
+
|
|
205
|
+
if p.get("annual_fee_tolerance"):
|
|
206
|
+
parts.append(f"Max annual fee: ${p['annual_fee_tolerance']}")
|
|
207
|
+
if p.get("points_programs"):
|
|
208
|
+
parts.append(f"Points programs: {p['points_programs']}")
|
|
209
|
+
if p.get("home_airport"):
|
|
210
|
+
parts.append(f"Home airport: {p['home_airport']}")
|
|
211
|
+
if p.get("goal"):
|
|
212
|
+
parts.append(f"Goal: {p['goal']}")
|
|
213
|
+
if p.get("preferences"):
|
|
214
|
+
parts.append(f"Preferences: {p['preferences']}")
|
|
215
|
+
if cards:
|
|
216
|
+
parts.append(f"Current cards: {', '.join(cards)}")
|
|
217
|
+
|
|
218
|
+
return " | ".join(parts) if parts else ""
|
|
219
|
+
|
|
220
|
+
|
|
136
221
|
# ---------------------------------------------------------------------------
|
|
137
222
|
# Migration
|
|
138
223
|
# ---------------------------------------------------------------------------
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fleece-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Credit card research and redemption CLI — live data via Brave Search, PointsYeah URL generation, designed for humans and AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
fleece_cli-0.3.1/CLAUDE.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Fleece - Credit Card Research Chatbot
|
|
2
|
-
|
|
3
|
-
## Project Overview
|
|
4
|
-
**Fleece** is a Streamlit-based conversational AI application designed to help users find the best credit cards for their needs. The tagline is "Find the best card for deal saviors."
|
|
5
|
-
|
|
6
|
-
### Core Purpose
|
|
7
|
-
Fleece is a chatbot that:
|
|
8
|
-
- Uses OpenAI's chat models (gpt-3.5-turbo, gpt-4, gpt-4-turbo, gpt-4o) to provide conversational credit card research and recommendations
|
|
9
|
-
- Maintains entity-based memory across conversations to remember user details and preferences
|
|
10
|
-
- Allows users to explore available credit cards and manage their existing card portfolio
|
|
11
|
-
- Stores conversation history and provides download capability
|
|
12
|
-
|
|
13
|
-
### Tech Stack
|
|
14
|
-
- **Frontend**: Streamlit (Python web app framework)
|
|
15
|
-
- **LLM**: OpenAI ChatGPT (via LangChain)
|
|
16
|
-
- **Memory**: ConversationEntityMemory (tracks entities mentioned in conversation)
|
|
17
|
-
- **Language**: Python
|
|
18
|
-
|
|
19
|
-
## Key Features
|
|
20
|
-
1. **Conversational Interface**: Chat with an AI assistant about credit cards
|
|
21
|
-
2. **Entity Memory**: Remembers details about the user across conversation turns
|
|
22
|
-
3. **Multi-model Support**: Choose between different OpenAI models
|
|
23
|
-
4. **Conversation Management**:
|
|
24
|
-
- Start new chats
|
|
25
|
-
- Browse past conversation sessions
|
|
26
|
-
- Download conversation history
|
|
27
|
-
- Customize memory buffer size (K parameter)
|
|
28
|
-
5. **Navigation**: Planned pages for "Credit Cards" exploration and "My Credit Cards" management
|
|
29
|
-
|
|
30
|
-
## Current Limitations & Opportunities
|
|
31
|
-
- **Limited knowledge**: Currently relies on OpenAI's training data, which may be outdated for credit card details
|
|
32
|
-
- **No specialized tools**: The chatbot cannot search current issuer websites or real-time data sources
|
|
33
|
-
- **No structured research**: Lacks the ability to provide specific, verified credit card information (fees, benefits, offers, transfer partners, etc.)
|
|
34
|
-
|
|
35
|
-
## Future Integration Opportunities
|
|
36
|
-
- **Multi-page app expansion**: Build out the "Credit Cards" discovery page and "My Credit Cards" portfolio management
|
|
37
|
-
- **Database backend**: Store user profiles, card portfolios, and preferences
|
|
38
|
-
|
|
39
|
-
## CLI — Redemption Commands
|
|
40
|
-
The `fleece flights` and `fleece hotels` commands are merged from the former `pointsyeah-cli` project (now archived). They generate PointsYeah search URLs with no API key required — pure stdlib, zero external dependencies. Source lives in `pointsyeah.py`.
|
|
41
|
-
|
|
42
|
-
## Development Notes
|
|
43
|
-
- Author: Yuan Chen
|
|
44
|
-
- Created: March 16, 2025
|
|
45
|
-
- Uses custom CSS styling (style.css)
|
|
46
|
-
- Requires OpenAI API key (entered via sidebar, not stored)
|
|
47
|
-
|
|
48
|
-
## Infrastructure Notes
|
|
49
|
-
- **Databricks**: No Databricks resources are available at the moment. The Databricks App deployment workflow has been removed. Do not suggest or implement Databricks-based solutions until resources are provisioned.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|