creatorloop 0.1.0__tar.gz → 0.2.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.
Files changed (56) hide show
  1. {creatorloop-0.1.0 → creatorloop-0.2.0}/CHANGELOG.md +15 -1
  2. {creatorloop-0.1.0 → creatorloop-0.2.0}/PKG-INFO +51 -15
  3. {creatorloop-0.1.0 → creatorloop-0.2.0}/README.md +50 -14
  4. {creatorloop-0.1.0 → creatorloop-0.2.0}/README.ru.md +4 -4
  5. {creatorloop-0.1.0 → creatorloop-0.2.0}/README.uz.md +4 -4
  6. {creatorloop-0.1.0 → creatorloop-0.2.0}/server.json +2 -2
  7. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/__init__.py +1 -1
  8. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/auth_cli.py +40 -11
  9. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/comments.py +3 -4
  10. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/config.py +2 -2
  11. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/env.example +6 -8
  12. creatorloop-0.2.0/src/creatorloop/env_file.py +35 -0
  13. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/graph_client.py +2 -1
  14. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/instagram.py +5 -2
  15. creatorloop-0.2.0/src/creatorloop/meta_auth.py +181 -0
  16. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/server.py +61 -3
  17. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/setup_cli.py +33 -4
  18. creatorloop-0.2.0/src/creatorloop/setup_i18n.py +187 -0
  19. creatorloop-0.2.0/src/creatorloop/setup_page.html +184 -0
  20. creatorloop-0.2.0/src/creatorloop/setup_web.py +377 -0
  21. creatorloop-0.2.0/tests/meta_fakes.py +44 -0
  22. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_api.py +12 -1
  23. creatorloop-0.2.0/tests/test_auth_cli.py +83 -0
  24. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_comments.py +3 -1
  25. creatorloop-0.2.0/tests/test_env_file.py +91 -0
  26. creatorloop-0.2.0/tests/test_meta_auth.py +216 -0
  27. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_server.py +154 -1
  28. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_setup_cli.py +93 -0
  29. creatorloop-0.2.0/tests/test_setup_i18n.py +47 -0
  30. creatorloop-0.2.0/tests/test_setup_web.py +413 -0
  31. {creatorloop-0.1.0 → creatorloop-0.2.0}/.gitignore +0 -0
  32. {creatorloop-0.1.0 → creatorloop-0.2.0}/LICENSE +0 -0
  33. {creatorloop-0.1.0 → creatorloop-0.2.0}/pyproject.toml +0 -0
  34. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/__main__.py +0 -0
  35. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/analytics.py +0 -0
  36. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/paths.py +0 -0
  37. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/post_queue.py +0 -0
  38. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/publish.py +0 -0
  39. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/refs.py +0 -0
  40. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/reviews.py +0 -0
  41. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/storage.py +0 -0
  42. {creatorloop-0.1.0 → creatorloop-0.2.0}/src/creatorloop/tunnel.py +0 -0
  43. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/__init__.py +0 -0
  44. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/conftest.py +0 -0
  45. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/fixtures/business_discovery_v26.json +0 -0
  46. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/fixtures/media_insights_reels_v26.json +0 -0
  47. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/fixtures/tiny-reel-faststart.mp4 +0 -0
  48. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_analytics.py +0 -0
  49. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_fixtures.py +0 -0
  50. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_paths.py +0 -0
  51. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_publish.py +0 -0
  52. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_queue.py +0 -0
  53. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_refs.py +0 -0
  54. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_release.py +0 -0
  55. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_reviews.py +0 -0
  56. {creatorloop-0.1.0 → creatorloop-0.2.0}/tests/test_tunnel.py +0 -0
@@ -5,6 +5,19 @@ All notable changes to this project are documented here. The format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.0] - 2026-10-14
9
+
10
+ ### Added
11
+ - `Dockerfile` to run the server without a local Python (settings and data in a `/data` volume, `cloudflared`
12
+ included); CI builds the image and checks that it answers MCP `initialize` and lists the tools.
13
+ - Setup page: `creatorloop-setup`, `creatorloop-auth login` and the new `open_setup` tool open a local page that
14
+ checks your Meta app, logs in with Facebook, finds your Instagram account and saves the settings — no Graph API
15
+ Explorer, no editing `.env`, no restart when started from Claude. English, Russian and Uzbek.
16
+
17
+ ### Changed
18
+ - `.env` is written atomically and readable by its owner only.
19
+ - Errors about missing settings or an expired token point to `open_setup` / `creatorloop-auth login`.
20
+
8
21
  ## [0.1.0] - 2026-10-13
9
22
 
10
23
  First public release.
@@ -22,5 +35,6 @@ First public release.
22
35
  - Three MCP prompts: weekly report, competitor brief, data-driven content ideas.
23
36
  - `examples/`: a sample carousel, a sample Reel storyboard and a Remotion Reel template with automatic checks.
24
37
 
25
- [Unreleased]: https://github.com/pandich93/creatorloop/compare/v0.1.0...HEAD
38
+ [Unreleased]: https://github.com/pandich93/creatorloop/compare/v0.2.0...HEAD
39
+ [0.2.0]: https://github.com/pandich93/creatorloop/compare/v0.1.0...v0.2.0
26
40
  [0.1.0]: https://github.com/pandich93/creatorloop/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: creatorloop
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Open-source AI content studio for Instagram creators: Claude analyses what works, helps make Reels and carousels, schedules and reviews them via the official Instagram API
5
5
  Project-URL: Homepage, https://github.com/pandich93/creatorloop
6
6
  Project-URL: Repository, https://github.com/pandich93/creatorloop
@@ -65,22 +65,17 @@ There are plenty of "Instagram API for Claude" servers. creatorloop is built for
65
65
 
66
66
  You need [uv](https://docs.astral.sh/uv/getting-started/installation/), [Claude Desktop](https://claude.ai/download),
67
67
  an Instagram **Business or Creator** account linked to a **Facebook Page**, and your own Meta app
68
- ([5-minute guide](https://github.com/pandich93/creatorloop/blob/main/docs/meta-setup.md)).
68
+ ([5-minute guide](https://github.com/pandich93/creatorloop/blob/main/docs/meta-setup.md#1-create-the-app)).
69
69
 
70
70
  ```bash
71
- uvx --from creatorloop creatorloop-setup # 1. connect to Claude Desktop, create the settings file
71
+ uvx --from creatorloop creatorloop-setup
72
72
  ```
73
73
 
74
- 2. Put your app's `META_APP_ID` and `META_APP_SECRET` into the settings file the command printed.
75
- 3. Save a 60-day token and find your account ID (the short-lived token comes from Graph API Explorer, see the guide):
74
+ It connects Claude Desktop and opens a setup page in your browser: enter your app's **App ID** and **App Secret**,
75
+ press **Continue with Facebook**, done. Then ask Claude: *"How did my last 10 posts do, and what should I post next?"*
76
76
 
77
- ```bash
78
- uvx --from creatorloop creatorloop-auth exchange --short-token <TOKEN> --write
79
- uvx --from creatorloop creatorloop-auth whoami --write
80
- uvx --from creatorloop creatorloop-auth check # → OK: @your_account followers=...
81
- ```
82
-
83
- Restart Claude Desktop and ask: *"How did my last 10 posts do, and what should I post next?"*
77
+ Already in Claude? Ask *"connect my Instagram"*: Claude opens the same page (`open_setup`). Your App Secret and
78
+ token are typed only into that local page, never into the chat.
84
79
 
85
80
  <details>
86
81
  <summary>Installing uv</summary>
@@ -98,6 +93,47 @@ uv downloads Python 3.14 by itself if you don't have it.
98
93
  keeps your other servers, makes a backup and creates your settings file. Claude Desktop rewrites its config while it
99
94
  runs, so on macOS the command quits and reopens it for you; on Windows and Linux it asks you to quit it first.
100
95
 
96
+ <details>
97
+ <summary>Running in Docker instead of uv</summary>
98
+
99
+ Build the image straight from GitHub and keep your settings in a folder on your computer:
100
+
101
+ ```bash
102
+ docker build -t creatorloop https://github.com/pandich93/creatorloop.git
103
+ mkdir "$HOME/creatorloop"
104
+ ```
105
+
106
+ If you use the post queue, first create `.env` in that folder with `IG_TIMEZONE=Europe/Berlin` (your zone): the container
107
+ runs in UTC.
108
+
109
+ Connect your account (the setup page runs in the container; open the printed link in your browser). Keep all three
110
+ `8765` the same; to use another port change all three:
111
+
112
+ ```bash
113
+ docker run --rm -it -p 127.0.0.1:8765:8765 -v "$HOME/creatorloop:/data" --entrypoint creatorloop-auth creatorloop \
114
+ login --no-browser --port 8765 --bind 0.0.0.0
115
+ ```
116
+
117
+ `creatorloop-setup` doesn't run inside Docker, so add the server to `claude_desktop_config.json` yourself, with the
118
+ full path to the folder:
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "creatorloop": {
124
+ "command": "docker",
125
+ "args": ["run", "-i", "--rm", "-v", "/Users/you/creatorloop:/data", "creatorloop"]
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ Post folders for `prepare_post` and `queue_post` go into `creatorloop/content/`. The image includes `cloudflared` for
132
+ publishing carousels.
133
+
134
+ The `open_setup` tool does not work inside Docker; use the command above to reconnect.
135
+ </details>
136
+
101
137
  ## What you can ask
102
138
 
103
139
  - "Make a report on my content for the last 60 days. Reels or carousels — what works better, and when should I post?"
@@ -121,9 +157,9 @@ Ready-made prompts in Claude Desktop's **+** menu: *Weekly Instagram report*, *C
121
157
  | Publishing | `prepare_post`, `get_post_job`, `publish_media`, `list_publications` |
122
158
  | Queue | `queue_post`, `list_queue`, `cancel_queued` |
123
159
  | Comments | `get_new_comments`, `reply_comment` |
124
- | Service | `get_api_status`, `clear_cache`, `restart_server` |
160
+ | Service | `get_api_status`, `open_setup`, `clear_cache`, `restart_server` |
125
161
 
126
- 33 tools in total. Tools that change something on Instagram are annotated as such for Claude, and their descriptions
162
+ 34 tools in total. Tools that change something on Instagram are annotated as such for Claude, and their descriptions
127
163
  tell it to act only after you approve: `publish_media` and `reply_comment` after you OK the preview or the exact
128
164
  text, `queue_post` only with `confirm=true` after you saw the preview.
129
165
 
@@ -187,7 +223,7 @@ Set `CREATORLOOP_HOME` to use another folder. The home folder holds `.env`, the
187
223
  | `IG_ACCESS_TOKEN` | yes | Long-lived (60 days) or System User (never expires) token — see [Meta setup](https://github.com/pandich93/creatorloop/blob/main/docs/meta-setup.md) |
188
224
  | `IG_USER_ID` | yes | Your Instagram professional account ID (`1784…`), not the username |
189
225
  | `GRAPH_API_VERSION` | no | Default `v26.0` |
190
- | `META_APP_ID`, `META_APP_SECRET` | no | Only for `creatorloop-auth exchange` |
226
+ | `META_APP_ID`, `META_APP_SECRET` | no | Filled by the setup page; also used to check the token expiry |
191
227
  | `IG_TIMEZONE` | no | IANA name, e.g. `Europe/Berlin`. Default: your computer's timezone |
192
228
  | `QUEUE_SLOTS` | no | e.g. `Mon,Wed,Fri 20:00` |
193
229
  | `IG_DB_PATH`, `CACHE_TTL_*_MIN` | no | Database location, cache lifetime in minutes |
@@ -36,22 +36,17 @@ There are plenty of "Instagram API for Claude" servers. creatorloop is built for
36
36
 
37
37
  You need [uv](https://docs.astral.sh/uv/getting-started/installation/), [Claude Desktop](https://claude.ai/download),
38
38
  an Instagram **Business or Creator** account linked to a **Facebook Page**, and your own Meta app
39
- ([5-minute guide](https://github.com/pandich93/creatorloop/blob/main/docs/meta-setup.md)).
39
+ ([5-minute guide](https://github.com/pandich93/creatorloop/blob/main/docs/meta-setup.md#1-create-the-app)).
40
40
 
41
41
  ```bash
42
- uvx --from creatorloop creatorloop-setup # 1. connect to Claude Desktop, create the settings file
42
+ uvx --from creatorloop creatorloop-setup
43
43
  ```
44
44
 
45
- 2. Put your app's `META_APP_ID` and `META_APP_SECRET` into the settings file the command printed.
46
- 3. Save a 60-day token and find your account ID (the short-lived token comes from Graph API Explorer, see the guide):
45
+ It connects Claude Desktop and opens a setup page in your browser: enter your app's **App ID** and **App Secret**,
46
+ press **Continue with Facebook**, done. Then ask Claude: *"How did my last 10 posts do, and what should I post next?"*
47
47
 
48
- ```bash
49
- uvx --from creatorloop creatorloop-auth exchange --short-token <TOKEN> --write
50
- uvx --from creatorloop creatorloop-auth whoami --write
51
- uvx --from creatorloop creatorloop-auth check # → OK: @your_account followers=...
52
- ```
53
-
54
- Restart Claude Desktop and ask: *"How did my last 10 posts do, and what should I post next?"*
48
+ Already in Claude? Ask *"connect my Instagram"*: Claude opens the same page (`open_setup`). Your App Secret and
49
+ token are typed only into that local page, never into the chat.
55
50
 
56
51
  <details>
57
52
  <summary>Installing uv</summary>
@@ -69,6 +64,47 @@ uv downloads Python 3.14 by itself if you don't have it.
69
64
  keeps your other servers, makes a backup and creates your settings file. Claude Desktop rewrites its config while it
70
65
  runs, so on macOS the command quits and reopens it for you; on Windows and Linux it asks you to quit it first.
71
66
 
67
+ <details>
68
+ <summary>Running in Docker instead of uv</summary>
69
+
70
+ Build the image straight from GitHub and keep your settings in a folder on your computer:
71
+
72
+ ```bash
73
+ docker build -t creatorloop https://github.com/pandich93/creatorloop.git
74
+ mkdir "$HOME/creatorloop"
75
+ ```
76
+
77
+ If you use the post queue, first create `.env` in that folder with `IG_TIMEZONE=Europe/Berlin` (your zone): the container
78
+ runs in UTC.
79
+
80
+ Connect your account (the setup page runs in the container; open the printed link in your browser). Keep all three
81
+ `8765` the same; to use another port change all three:
82
+
83
+ ```bash
84
+ docker run --rm -it -p 127.0.0.1:8765:8765 -v "$HOME/creatorloop:/data" --entrypoint creatorloop-auth creatorloop \
85
+ login --no-browser --port 8765 --bind 0.0.0.0
86
+ ```
87
+
88
+ `creatorloop-setup` doesn't run inside Docker, so add the server to `claude_desktop_config.json` yourself, with the
89
+ full path to the folder:
90
+
91
+ ```json
92
+ {
93
+ "mcpServers": {
94
+ "creatorloop": {
95
+ "command": "docker",
96
+ "args": ["run", "-i", "--rm", "-v", "/Users/you/creatorloop:/data", "creatorloop"]
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ Post folders for `prepare_post` and `queue_post` go into `creatorloop/content/`. The image includes `cloudflared` for
103
+ publishing carousels.
104
+
105
+ The `open_setup` tool does not work inside Docker; use the command above to reconnect.
106
+ </details>
107
+
72
108
  ## What you can ask
73
109
 
74
110
  - "Make a report on my content for the last 60 days. Reels or carousels — what works better, and when should I post?"
@@ -92,9 +128,9 @@ Ready-made prompts in Claude Desktop's **+** menu: *Weekly Instagram report*, *C
92
128
  | Publishing | `prepare_post`, `get_post_job`, `publish_media`, `list_publications` |
93
129
  | Queue | `queue_post`, `list_queue`, `cancel_queued` |
94
130
  | Comments | `get_new_comments`, `reply_comment` |
95
- | Service | `get_api_status`, `clear_cache`, `restart_server` |
131
+ | Service | `get_api_status`, `open_setup`, `clear_cache`, `restart_server` |
96
132
 
97
- 33 tools in total. Tools that change something on Instagram are annotated as such for Claude, and their descriptions
133
+ 34 tools in total. Tools that change something on Instagram are annotated as such for Claude, and their descriptions
98
134
  tell it to act only after you approve: `publish_media` and `reply_comment` after you OK the preview or the exact
99
135
  text, `queue_post` only with `confirm=true` after you saw the preview.
100
136
 
@@ -158,7 +194,7 @@ Set `CREATORLOOP_HOME` to use another folder. The home folder holds `.env`, the
158
194
  | `IG_ACCESS_TOKEN` | yes | Long-lived (60 days) or System User (never expires) token — see [Meta setup](https://github.com/pandich93/creatorloop/blob/main/docs/meta-setup.md) |
159
195
  | `IG_USER_ID` | yes | Your Instagram professional account ID (`1784…`), not the username |
160
196
  | `GRAPH_API_VERSION` | no | Default `v26.0` |
161
- | `META_APP_ID`, `META_APP_SECRET` | no | Only for `creatorloop-auth exchange` |
197
+ | `META_APP_ID`, `META_APP_SECRET` | no | Filled by the setup page; also used to check the token expiry |
162
198
  | `IG_TIMEZONE` | no | IANA name, e.g. `Europe/Berlin`. Default: your computer's timezone |
163
199
  | `QUEUE_SLOTS` | no | e.g. `Mon,Wed,Fri 20:00` |
164
200
  | `IG_DB_PATH`, `CACHE_TTL_*_MIN` | no | Database location, cache lifetime in minutes |
@@ -22,12 +22,12 @@
22
22
 
23
23
  ```bash
24
24
  uvx --from creatorloop creatorloop-setup
25
- uvx --from creatorloop creatorloop-auth exchange --short-token <ТОКЕН> --write
26
- uvx --from creatorloop creatorloop-auth whoami --write
27
25
  ```
28
26
 
29
- Перед второй командой впишите `META_APP_ID` и `META_APP_SECRET` в файл настроек, путь к которому покажет первая.
30
- Перезапустите Claude Desktop и спросите: *«Как зашли мои последние 10 постов и что мне публиковать дальше?»*
27
+ Команда подключит Claude Desktop и откроет страницу настройки в браузере: введите **App ID** и **App Secret**
28
+ своего приложения, нажмите **«Продолжить через Facebook»** — готово. Затем спросите Claude: *«Как зашли мои
29
+ последние 10 постов и что мне публиковать дальше?»* Уже в Claude? Попросите *«подключи мой Instagram»* — откроется
30
+ та же страница. Секрет и токен вводятся только на ней, не в чат.
31
31
 
32
32
  Полная документация — в [README на английском](README.md).
33
33
 
@@ -22,12 +22,12 @@ Kerak boʻladi: [uv](https://docs.astral.sh/uv/getting-started/installation/), C
22
22
 
23
23
  ```bash
24
24
  uvx --from creatorloop creatorloop-setup
25
- uvx --from creatorloop creatorloop-auth exchange --short-token <TOKEN> --write
26
- uvx --from creatorloop creatorloop-auth whoami --write
27
25
  ```
28
26
 
29
- Ikkinchi buyruqdan oldin birinchi buyruq koʻrsatgan sozlamalar fayliga `META_APP_ID` va `META_APP_SECRET` ni yozing.
30
- Claude Desktop'ni qayta ishga tushiring va soʻrang: *«Oxirgi 10 ta postim qanday ketdi va keyin nima joylashim kerak?»*
27
+ Buyruq Claude Desktop'ni ulaydi va brauzerda sozlash sahifasini ochadi: ilovangizning **App ID** va **App Secret**
28
+ qiymatlarini kiriting, **“Facebook orqali davom etish”** tugmasini bosing — tayyor. Soʻng Claude'dan soʻrang:
29
+ *«Oxirgi 10 ta postim qanday ketdi va keyin nima joylashim kerak?»* Claude'dasizmi? *«Instagram'imni ulab ber»* deb
30
+ soʻrang — oʻsha sahifa ochiladi. Secret va token faqat shu sahifada kiritiladi, chatga emas.
31
31
 
32
32
  Toʻliq hujjatlar — [ingliz tilidagi README](README.md)da.
33
33
 
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/pandich93/creatorloop",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.1.0",
10
+ "version": "0.2.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "creatorloop",
15
- "version": "0.1.0",
15
+ "version": "0.2.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -5,7 +5,7 @@ analyse your account and competitors, make Reels and carousels, schedule them an
5
5
  Not affiliated with, endorsed or sponsored by Meta or Instagram.
6
6
  """
7
7
 
8
- __version__ = "0.1.0"
8
+ __version__ = "0.2.0"
9
9
 
10
10
  DISCLAIMER = (
11
11
  "creatorloop is an independent open-source project. Not affiliated with, endorsed or sponsored by Meta or "
@@ -1,6 +1,7 @@
1
- """Helper CLI: exchange tokens and find your Instagram professional account ID.
1
+ """Helper CLI: connect your Instagram account, exchange tokens and find your account ID.
2
2
 
3
3
  Usage:
4
+ uv run creatorloop-auth login [--no-browser] [--port N] [--bind ADDR]
4
5
  uv run creatorloop-auth exchange --short-token <TOKEN> [--write]
5
6
  uv run creatorloop-auth whoami [--write]
6
7
  uv run creatorloop-auth check
@@ -15,7 +16,7 @@ from pathlib import Path
15
16
 
16
17
  import httpx
17
18
 
18
- from creatorloop import DISCLAIMER, paths
19
+ from creatorloop import DISCLAIMER, env_file, paths, setup_web
19
20
  from creatorloop.config import load_settings
20
21
 
21
22
  ENV_PATH = paths.home_dir() / ".env"
@@ -31,18 +32,40 @@ def _get(url: str, params: dict[str, str]) -> dict:
31
32
 
32
33
 
33
34
  def _write_env(key: str, value: str, path: Path = ENV_PATH) -> None:
34
- path.parent.mkdir(parents=True, exist_ok=True)
35
- lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
36
- for i, line in enumerate(lines):
37
- if line.split("=", 1)[0].strip() == key:
38
- lines[i] = f"{key}={value}"
39
- break
40
- else:
41
- lines.append(f"{key}={value}")
42
- path.write_text("\n".join(lines) + "\n", encoding="utf-8")
35
+ env_file.update(path, {key: value})
43
36
  print(f" -> saved {key} to {path}")
44
37
 
45
38
 
39
+ def cmd_login(args: argparse.Namespace) -> None:
40
+ s = load_settings()
41
+ env_path = paths.home_dir() / ".env"
42
+ if args.bind not in {"127.0.0.1", "localhost", "::1"}:
43
+ print(
44
+ f"Listening on {args.bind}: the page is reachable from the network. That is expected inside Docker with "
45
+ "-p 127.0.0.1:PORT:PORT; otherwise keep the default 127.0.0.1.",
46
+ file=sys.stderr,
47
+ )
48
+ try:
49
+ result = setup_web.run(
50
+ env_path,
51
+ open_browser=not args.no_browser,
52
+ port=args.port,
53
+ bind=args.bind,
54
+ api_version=s.api_version,
55
+ app_id=s.app_id or "",
56
+ app_secret=s.app_secret or "",
57
+ )
58
+ except OSError as exc:
59
+ sys.exit(f"Cannot start the setup page on {args.bind}:{args.port}: {exc}. Pass another --port, or check --bind.")
60
+ if result is None:
61
+ sys.exit(
62
+ "The setup page closed before the account was connected (stopped, or the 15-minute limit). "
63
+ "Run `creatorloop-auth login` again."
64
+ )
65
+ print(f"OK: @{result.username} connected, settings saved to {env_path}. "
66
+ f"Restart Claude Desktop if it is running.")
67
+
68
+
46
69
  def cmd_exchange(args: argparse.Namespace) -> None:
47
70
  s = load_settings()
48
71
  if not (s.app_id and s.app_secret):
@@ -108,6 +131,12 @@ def main() -> None:
108
131
  parser = argparse.ArgumentParser(prog="creatorloop-auth", description=__doc__.split("\n")[0], epilog=DISCLAIMER)
109
132
  sub = parser.add_subparsers(required=True)
110
133
 
134
+ p = sub.add_parser("login", help="Connect your Instagram account on a local page in the browser (recommended)")
135
+ p.add_argument("--no-browser", action="store_true", help="Only print the link (e.g. in Docker)")
136
+ p.add_argument("--port", type=int, default=0, help="Port of the setup page (default: any free port)")
137
+ p.add_argument("--bind", default="127.0.0.1", help="Listen address; 0.0.0.0 only inside Docker with -p 127.0.0.1:PORT:PORT")
138
+ p.set_defaults(func=cmd_login)
139
+
111
140
  p = sub.add_parser("exchange", help="Exchange short-lived user token for a long-lived (60 days) one")
112
141
  p.add_argument("--short-token", required=True)
113
142
  p.add_argument("--write", action="store_true", help="Save to .env instead of printing")
@@ -1,6 +1,6 @@
1
1
  """Comments on own posts: digest of new / unanswered comments and replies (only after user approval).
2
2
 
3
- Needs the token permission instagram_manage_comments (README, section 3).
3
+ Needs the token permission instagram_manage_comments (kept ticked on the setup page, see docs/meta-setup.md).
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -23,9 +23,8 @@ class CommentsError(RuntimeError):
23
23
  def _permission_hint(exc: GraphAPIError) -> CommentsError:
24
24
  if exc.code in PERMISSION_CODES or "permission" in exc.message.lower():
25
25
  return CommentsError(
26
- "The token has no instagram_manage_comments permission. Regenerate it in Graph API Explorer with "
27
- "instagram_manage_comments added, then run `uv run creatorloop-auth exchange --short-token <TOKEN> "
28
- f"--write` (README, section 3). Graph API said: {exc}"
26
+ "The token has no instagram_manage_comments permission. Reconnect with open_setup (again=true) "
27
+ f"and keep instagram_manage_comments ticked. Graph API said: {exc}"
29
28
  )
30
29
  return CommentsError(f"Graph API error: {exc}")
31
30
 
@@ -78,8 +78,8 @@ class Settings:
78
78
  ]
79
79
  if missing:
80
80
  raise RuntimeError(
81
- f"Missing required settings: {', '.join(missing)}. "
82
- f"Fill them in {paths.home_dir() / '.env'} (run creatorloop-setup to create it)."
81
+ f"Missing required settings: {', '.join(missing)}. Connect your Instagram account: call the open_setup "
82
+ "tool, or run `uvx --from creatorloop creatorloop-auth login` in a terminal."
83
83
  )
84
84
 
85
85
 
@@ -1,19 +1,17 @@
1
- # creatorloop settings. `creatorloop-setup` creates this file in your creatorloop home folder;
2
- # fill in the two required values below and restart Claude Desktop.
3
- # Commands below are for a PyPI install; from a source checkout use `uv run creatorloop-auth ...`.
1
+ # creatorloop settings. `creatorloop-setup` creates this file in your creatorloop home folder and fills
2
+ # IG_ACCESS_TOKEN, IG_USER_ID, META_APP_ID and META_APP_SECRET through the setup page in your browser.
3
+ # Connect again any time: `uvx --from creatorloop creatorloop-auth login` (or ask Claude to run open_setup).
4
4
 
5
- # Long-lived user access token (60 days) or a System User token (never expires).
6
- # Get it with: uvx --from creatorloop creatorloop-auth exchange --short-token <TOKEN> --write
5
+ # Long-lived user access token (60 days) or a System User token (never expires). Filled by the setup page.
7
6
  IG_ACCESS_TOKEN=
8
7
 
9
- # Instagram professional account ID (NOT the username). 17841400000000000-like number.
10
- # Get it with: uvx --from creatorloop creatorloop-auth whoami --write
8
+ # Instagram professional account ID (NOT the username). 17841400000000000-like number. Filled by the setup page.
11
9
  IG_USER_ID=
12
10
 
13
11
  # Graph API version. Check https://developers.facebook.com/docs/graph-api/changelog
14
12
  GRAPH_API_VERSION=v26.0
15
13
 
16
- # Optional: needed only for the token exchange and token expiry check.
14
+ # Your Meta app. Filled by the setup page; also used for the token expiry check.
17
15
  META_APP_ID=
18
16
  META_APP_SECRET=
19
17
 
@@ -0,0 +1,35 @@
1
+ """Update KEY=value lines of the creatorloop .env without touching other lines or comments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import os
7
+ import tempfile
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+
11
+
12
+ def update(path: Path, values: Mapping[str, str]) -> None:
13
+ """Set the given keys (every existing line of a key is replaced, missing keys are appended) in one atomic write.
14
+ The file is readable by its owner only on POSIX: it holds the App Secret and the access token."""
15
+ path.parent.mkdir(parents=True, exist_ok=True)
16
+ lines = path.read_text(encoding="utf-8-sig").splitlines() if path.exists() else [] # -sig: Notepad's BOM
17
+ seen: set[str] = set()
18
+ for i, line in enumerate(lines):
19
+ key = line.split("=", 1)[0].strip()
20
+ if "=" in line and not line.lstrip().startswith("#") and key in values:
21
+ lines[i] = f"{key}={values[key]}"
22
+ seen.add(key)
23
+ lines += [f"{key}={value}" for key, value in values.items() if key not in seen]
24
+ fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".env.", suffix=".tmp") # mkstemp creates it with 0600
25
+ try:
26
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
27
+ f.write("\n".join(lines) + "\n")
28
+ if hasattr(os, "geteuid") and os.geteuid() == 0: # a root container must not turn the user's .env into root:root
29
+ owner = path.stat() if path.exists() else path.parent.stat()
30
+ with contextlib.suppress(OSError):
31
+ os.chown(tmp, owner.st_uid, owner.st_gid)
32
+ os.replace(tmp, path)
33
+ except BaseException:
34
+ Path(tmp).unlink(missing_ok=True)
35
+ raise
@@ -218,7 +218,8 @@ class GraphClient:
218
218
  )
219
219
  if code == TOKEN_ERROR_CODE:
220
220
  raise TokenError(
221
- f"Access token problem: {message}. Refresh IG_ACCESS_TOKEN (README, section 'Token').",
221
+ f"Access token problem: {message}. Reconnect the account: call open_setup with again=true "
222
+ "(or run `creatorloop-auth login`).",
222
223
  **kwargs,
223
224
  )
224
225
  raise GraphAPIError(message, **kwargs)
@@ -406,11 +406,14 @@ class InstagramService:
406
406
  }
407
407
  lefts = [d for d in (days_left, data_days_left) if d is not None]
408
408
  if not data.get("is_valid"):
409
- status["warning"] = "Token is invalid. Generate a new one (README, section 'Token')."
409
+ status["warning"] = (
410
+ "Token is invalid. Reconnect: call open_setup with again=true (or run `creatorloop-auth login`)."
411
+ )
410
412
  elif lefts and min(lefts) <= TOKEN_WARN_DAYS:
411
413
  status["warning"] = (
412
414
  f"Token or its data access expires in {min(lefts)} days. "
413
- "Renew it or switch to a never-expiring System User token (README, section 'Token')."
415
+ "Renew: call open_setup with again=true, or use a never-expiring System User token "
416
+ "(docs/meta-setup.md, step 3)."
414
417
  )
415
418
  return status
416
419