insim-cli 1.0.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 (34) hide show
  1. insim_cli-1.0.0/PKG-INFO +263 -0
  2. insim_cli-1.0.0/cli_anything/insim/README.md +226 -0
  3. insim_cli-1.0.0/cli_anything/insim/__init__.py +2 -0
  4. insim_cli-1.0.0/cli_anything/insim/core/__init__.py +0 -0
  5. insim_cli-1.0.0/cli_anything/insim/core/account.py +67 -0
  6. insim_cli-1.0.0/cli_anything/insim/core/api.py +68 -0
  7. insim_cli-1.0.0/cli_anything/insim/core/auth.py +74 -0
  8. insim_cli-1.0.0/cli_anything/insim/core/calls.py +93 -0
  9. insim_cli-1.0.0/cli_anything/insim/core/campaigns.py +141 -0
  10. insim_cli-1.0.0/cli_anything/insim/core/contacts.py +231 -0
  11. insim_cli-1.0.0/cli_anything/insim/core/lists.py +192 -0
  12. insim_cli-1.0.0/cli_anything/insim/core/qualifications.py +139 -0
  13. insim_cli-1.0.0/cli_anything/insim/core/sms.py +158 -0
  14. insim_cli-1.0.0/cli_anything/insim/core/stats.py +53 -0
  15. insim_cli-1.0.0/cli_anything/insim/core/templates.py +137 -0
  16. insim_cli-1.0.0/cli_anything/insim/insim_cli.py +194 -0
  17. insim_cli-1.0.0/cli_anything/insim/skills/SKILL.md +617 -0
  18. insim_cli-1.0.0/cli_anything/insim/utils/__init__.py +0 -0
  19. insim_cli-1.0.0/cli_anything/insim/utils/output.py +112 -0
  20. insim_cli-1.0.0/insim_cli.egg-info/PKG-INFO +263 -0
  21. insim_cli-1.0.0/insim_cli.egg-info/SOURCES.txt +32 -0
  22. insim_cli-1.0.0/insim_cli.egg-info/dependency_links.txt +1 -0
  23. insim_cli-1.0.0/insim_cli.egg-info/entry_points.txt +2 -0
  24. insim_cli-1.0.0/insim_cli.egg-info/not-zip-safe +1 -0
  25. insim_cli-1.0.0/insim_cli.egg-info/requires.txt +7 -0
  26. insim_cli-1.0.0/insim_cli.egg-info/top_level.txt +1 -0
  27. insim_cli-1.0.0/setup.cfg +4 -0
  28. insim_cli-1.0.0/setup.py +54 -0
  29. insim_cli-1.0.0/tests/test_auth.py +67 -0
  30. insim_cli-1.0.0/tests/test_calls.py +35 -0
  31. insim_cli-1.0.0/tests/test_campaigns.py +51 -0
  32. insim_cli-1.0.0/tests/test_contacts.py +94 -0
  33. insim_cli-1.0.0/tests/test_integration.py +79 -0
  34. insim_cli-1.0.0/tests/test_sms.py +51 -0
@@ -0,0 +1,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: insim-cli
3
+ Version: 1.0.0
4
+ Summary: inSIM CLI — Control your SMS, contacts and campaigns from the command line. Built for AI agents and humans alike.
5
+ Home-page: https://github.com/ArdaryinSIM/insim-cli
6
+ Author: Reach Technologies SAS
7
+ Author-email: dev@reach-technologies.com
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: Topic :: Communications :: Telephony
12
+ Classifier: Topic :: Office/Business
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: click>=8.0.0
22
+ Requires-Dist: requests>=2.28.0
23
+ Requires-Dist: prompt-toolkit>=3.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
27
+ Dynamic: author
28
+ Dynamic: author-email
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: provides-extra
34
+ Dynamic: requires-dist
35
+ Dynamic: requires-python
36
+ Dynamic: summary
37
+
38
+ # insim-cli
39
+
40
+ **Control your SMS, contacts, and campaigns from the command line.**
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/insim-cli.svg)](https://pypi.org/project/insim-cli/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/insim-cli.svg)](https://pypi.org/project/insim-cli/)
44
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
45
+
46
+ ---
47
+
48
+ ## Why insim-cli?
49
+
50
+ **Stop switching between tabs.** Manage your entire SMS business from your terminal.
51
+
52
+ - **Send SMS in seconds** — one command, done. No login pages, no clicks.
53
+ - **AI-ready** — Claude, GPT, Gemini, and any LLM can pilot inSIM with structured JSON output.
54
+ - **Automate your campaigns** — create lists, compose messages, and launch campaigns programmatically.
55
+ - **Track everything** — call logs, delivery reports, click tracking, all at your fingertips.
56
+ - **Works everywhere** — macOS, Linux, Windows. Any terminal. Any automation tool.
57
+
58
+ ## Quick Start (2 minutes)
59
+
60
+ ### 1. Install
61
+
62
+ ```bash
63
+ pip install insim-cli
64
+ ```
65
+
66
+ ### 2. Login
67
+
68
+ ```bash
69
+ insim login your@email.com --key YOUR_ACCESS_KEY
70
+ ```
71
+
72
+ Your access key is in your inSIM account settings, under API.
73
+
74
+ ### 3. Start using
75
+
76
+ ```bash
77
+ # Find a contact
78
+ insim contacts search "John Doe"
79
+
80
+ # Send an SMS
81
+ insim sms send "+33612345678" "Hello from the command line!"
82
+
83
+ # Check your stats
84
+ insim stats overview
85
+ ```
86
+
87
+ That's it. You're ready.
88
+
89
+ ---
90
+
91
+ ## What can you do?
92
+
93
+ ### Send SMS
94
+
95
+ ```bash
96
+ # Simple message
97
+ insim sms send "+33612345678" "Meeting confirmed for tomorrow at 10am"
98
+
99
+ # With link tracking
100
+ insim sms send "+33612345678" "Check our offer: [link]" --url "https://your-site.com/promo"
101
+ ```
102
+
103
+ ### Manage Contacts
104
+
105
+ ```bash
106
+ # Smart search (handles typos, inversions, accents)
107
+ insim contacts search "cecile dubois"
108
+
109
+ # Find by phone number
110
+ insim contacts find-phone "0612345678"
111
+
112
+ # Tag contacts for segmentation
113
+ insim contacts tags-add CONTACT_ID vip premium
114
+ ```
115
+
116
+ ### Launch Campaigns
117
+
118
+ ```bash
119
+ # Create a list with all your contacts
120
+ insim lists create "Spring Promo"
121
+ insim lists add-all LIST_ID
122
+
123
+ # Create and launch a campaign
124
+ insim campaigns create --name "Spring Promo" --message "20% off this week!" --list LIST_ID
125
+ insim campaigns start CAMPAIGN_ID
126
+ ```
127
+
128
+ ### Track Calls
129
+
130
+ ```bash
131
+ # See missed calls
132
+ insim calls list --type missed
133
+
134
+ # Qualify a call
135
+ insim calls qualify CALL_ID --option OPTION_ID --notes "Interested in premium plan"
136
+ ```
137
+
138
+ ### View Statistics
139
+
140
+ ```bash
141
+ # Global overview
142
+ insim stats overview
143
+
144
+ # SMS sent this month
145
+ insim stats overview --from "2026-04-01"
146
+ ```
147
+
148
+ ---
149
+
150
+ ## For AI Agents
151
+
152
+ insim-cli is designed to be used by AI coding agents like **Claude Code**, **GitHub Copilot**, **Cursor**, and others.
153
+
154
+ ### Setup for agents
155
+
156
+ ```bash
157
+ export INSIM_LOGIN="your@email.com"
158
+ export INSIM_ACCESS_KEY="your_access_key"
159
+ ```
160
+
161
+ ### JSON output
162
+
163
+ Add `--json` to any command for machine-readable output:
164
+
165
+ ```bash
166
+ insim --json contacts search "john doe"
167
+ insim --json sms list --limit 5
168
+ insim --json account info
169
+ ```
170
+
171
+ ### AI workflow example
172
+
173
+ An agent receiving "send a message to John saying meeting is confirmed":
174
+
175
+ ```bash
176
+ # Step 1: Find the contact
177
+ insim --json contacts search "john"
178
+ # → {"contacts": [{"phone_number": "+33612345678", "score": 100}]}
179
+
180
+ # Step 2: Send the SMS
181
+ insim sms send "+33612345678" "Meeting is confirmed"
182
+ # → SMS sent successfully!
183
+ ```
184
+
185
+ ### SKILL.md
186
+
187
+ The package includes a comprehensive `SKILL.md` file with **25 detailed workflows** that any AI agent can read to understand exactly how to use every feature of insim-cli. The REPL mode displays the path to this file on startup.
188
+
189
+ ---
190
+
191
+ ## Interactive Mode (REPL)
192
+
193
+ Run `insim` without arguments for an interactive session with **auto-completion** and **command history**:
194
+
195
+ ```
196
+ $ insim
197
+ SKILL.md: /path/to/skills/SKILL.md
198
+ inSIM CLI — Interactive Mode
199
+ Type commands without 'insim' prefix. Type 'help' or 'exit'.
200
+
201
+ insim> contacts search "mourad"
202
+ insim> sms send "+33664456336" "Hello!"
203
+ insim> stats overview
204
+ insim> exit
205
+ ```
206
+
207
+ ---
208
+
209
+ ## All Commands
210
+
211
+ | Group | Commands |
212
+ |-------|----------|
213
+ | **Auth** | `login`, `logout`, `whoami` |
214
+ | **Contacts** | `list`, `search`, `find-phone`, `detail`, `switch-pro`, `delete`, `tags-add`, `tags-remove`, `custom-fields` |
215
+ | **SMS** | `list`, `detail`, `conversation`, `send` |
216
+ | **Calls** | `list`, `qualify`, `clictocall` |
217
+ | **Qualifications** | `list`, `options`, `options-create`, `options-update`, `options-delete`, `stats` |
218
+ | **Account** | `info`, `webhooks-set` |
219
+ | **Lists** | `list`, `create`, `detail`, `update`, `delete`, `add-contacts`, `remove-contacts`, `add-all` |
220
+ | **Campaigns** | `list`, `create`, `detail`, `cancel`, `start` |
221
+ | **Templates** | `list`, `create`, `update`, `delete`, `send` |
222
+ | **Stats** | `overview`, `clicks` |
223
+
224
+ Run `insim COMMAND --help` for details on any command.
225
+
226
+ ---
227
+
228
+ ## Configuration
229
+
230
+ | Method | Priority | Description |
231
+ |--------|----------|-------------|
232
+ | Environment variables | Highest | `INSIM_LOGIN`, `INSIM_ACCESS_KEY` |
233
+ | Credentials file | Lower | `~/.insim/credentials.json` (created by `insim login`) |
234
+
235
+ Custom server URL:
236
+ ```bash
237
+ export INSIM_BASE_URL="https://custom-server.com"
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Development
243
+
244
+ ```bash
245
+ git clone https://github.com/ArdaryinSIM/insim-cli.git
246
+ cd insim-cli
247
+ pip install -e ".[dev]"
248
+ pytest tests/ -v
249
+ ```
250
+
251
+ ---
252
+
253
+ ## About inSIM
254
+
255
+ [inSIM](https://insim.app) is a SaaS platform for SMS management, CRM, and marketing campaigns. It connects to your phone via a mobile app and lets you send, receive, and track SMS from your computer.
256
+
257
+ **insim-cli** is the official command-line interface, built by [Reach Technologies SAS](https://reach-technologies.com).
258
+
259
+ ---
260
+
261
+ ## License
262
+
263
+ MIT
@@ -0,0 +1,226 @@
1
+ # insim-cli
2
+
3
+ **Control your SMS, contacts, and campaigns from the command line.**
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/insim-cli.svg)](https://pypi.org/project/insim-cli/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/insim-cli.svg)](https://pypi.org/project/insim-cli/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ---
10
+
11
+ ## Why insim-cli?
12
+
13
+ **Stop switching between tabs.** Manage your entire SMS business from your terminal.
14
+
15
+ - **Send SMS in seconds** — one command, done. No login pages, no clicks.
16
+ - **AI-ready** — Claude, GPT, Gemini, and any LLM can pilot inSIM with structured JSON output.
17
+ - **Automate your campaigns** — create lists, compose messages, and launch campaigns programmatically.
18
+ - **Track everything** — call logs, delivery reports, click tracking, all at your fingertips.
19
+ - **Works everywhere** — macOS, Linux, Windows. Any terminal. Any automation tool.
20
+
21
+ ## Quick Start (2 minutes)
22
+
23
+ ### 1. Install
24
+
25
+ ```bash
26
+ pip install insim-cli
27
+ ```
28
+
29
+ ### 2. Login
30
+
31
+ ```bash
32
+ insim login your@email.com --key YOUR_ACCESS_KEY
33
+ ```
34
+
35
+ Your access key is in your inSIM account settings, under API.
36
+
37
+ ### 3. Start using
38
+
39
+ ```bash
40
+ # Find a contact
41
+ insim contacts search "John Doe"
42
+
43
+ # Send an SMS
44
+ insim sms send "+33612345678" "Hello from the command line!"
45
+
46
+ # Check your stats
47
+ insim stats overview
48
+ ```
49
+
50
+ That's it. You're ready.
51
+
52
+ ---
53
+
54
+ ## What can you do?
55
+
56
+ ### Send SMS
57
+
58
+ ```bash
59
+ # Simple message
60
+ insim sms send "+33612345678" "Meeting confirmed for tomorrow at 10am"
61
+
62
+ # With link tracking
63
+ insim sms send "+33612345678" "Check our offer: [link]" --url "https://your-site.com/promo"
64
+ ```
65
+
66
+ ### Manage Contacts
67
+
68
+ ```bash
69
+ # Smart search (handles typos, inversions, accents)
70
+ insim contacts search "cecile dubois"
71
+
72
+ # Find by phone number
73
+ insim contacts find-phone "0612345678"
74
+
75
+ # Tag contacts for segmentation
76
+ insim contacts tags-add CONTACT_ID vip premium
77
+ ```
78
+
79
+ ### Launch Campaigns
80
+
81
+ ```bash
82
+ # Create a list with all your contacts
83
+ insim lists create "Spring Promo"
84
+ insim lists add-all LIST_ID
85
+
86
+ # Create and launch a campaign
87
+ insim campaigns create --name "Spring Promo" --message "20% off this week!" --list LIST_ID
88
+ insim campaigns start CAMPAIGN_ID
89
+ ```
90
+
91
+ ### Track Calls
92
+
93
+ ```bash
94
+ # See missed calls
95
+ insim calls list --type missed
96
+
97
+ # Qualify a call
98
+ insim calls qualify CALL_ID --option OPTION_ID --notes "Interested in premium plan"
99
+ ```
100
+
101
+ ### View Statistics
102
+
103
+ ```bash
104
+ # Global overview
105
+ insim stats overview
106
+
107
+ # SMS sent this month
108
+ insim stats overview --from "2026-04-01"
109
+ ```
110
+
111
+ ---
112
+
113
+ ## For AI Agents
114
+
115
+ insim-cli is designed to be used by AI coding agents like **Claude Code**, **GitHub Copilot**, **Cursor**, and others.
116
+
117
+ ### Setup for agents
118
+
119
+ ```bash
120
+ export INSIM_LOGIN="your@email.com"
121
+ export INSIM_ACCESS_KEY="your_access_key"
122
+ ```
123
+
124
+ ### JSON output
125
+
126
+ Add `--json` to any command for machine-readable output:
127
+
128
+ ```bash
129
+ insim --json contacts search "john doe"
130
+ insim --json sms list --limit 5
131
+ insim --json account info
132
+ ```
133
+
134
+ ### AI workflow example
135
+
136
+ An agent receiving "send a message to John saying meeting is confirmed":
137
+
138
+ ```bash
139
+ # Step 1: Find the contact
140
+ insim --json contacts search "john"
141
+ # → {"contacts": [{"phone_number": "+33612345678", "score": 100}]}
142
+
143
+ # Step 2: Send the SMS
144
+ insim sms send "+33612345678" "Meeting is confirmed"
145
+ # → SMS sent successfully!
146
+ ```
147
+
148
+ ### SKILL.md
149
+
150
+ The package includes a comprehensive `SKILL.md` file with **25 detailed workflows** that any AI agent can read to understand exactly how to use every feature of insim-cli. The REPL mode displays the path to this file on startup.
151
+
152
+ ---
153
+
154
+ ## Interactive Mode (REPL)
155
+
156
+ Run `insim` without arguments for an interactive session with **auto-completion** and **command history**:
157
+
158
+ ```
159
+ $ insim
160
+ SKILL.md: /path/to/skills/SKILL.md
161
+ inSIM CLI — Interactive Mode
162
+ Type commands without 'insim' prefix. Type 'help' or 'exit'.
163
+
164
+ insim> contacts search "mourad"
165
+ insim> sms send "+33664456336" "Hello!"
166
+ insim> stats overview
167
+ insim> exit
168
+ ```
169
+
170
+ ---
171
+
172
+ ## All Commands
173
+
174
+ | Group | Commands |
175
+ |-------|----------|
176
+ | **Auth** | `login`, `logout`, `whoami` |
177
+ | **Contacts** | `list`, `search`, `find-phone`, `detail`, `switch-pro`, `delete`, `tags-add`, `tags-remove`, `custom-fields` |
178
+ | **SMS** | `list`, `detail`, `conversation`, `send` |
179
+ | **Calls** | `list`, `qualify`, `clictocall` |
180
+ | **Qualifications** | `list`, `options`, `options-create`, `options-update`, `options-delete`, `stats` |
181
+ | **Account** | `info`, `webhooks-set` |
182
+ | **Lists** | `list`, `create`, `detail`, `update`, `delete`, `add-contacts`, `remove-contacts`, `add-all` |
183
+ | **Campaigns** | `list`, `create`, `detail`, `cancel`, `start` |
184
+ | **Templates** | `list`, `create`, `update`, `delete`, `send` |
185
+ | **Stats** | `overview`, `clicks` |
186
+
187
+ Run `insim COMMAND --help` for details on any command.
188
+
189
+ ---
190
+
191
+ ## Configuration
192
+
193
+ | Method | Priority | Description |
194
+ |--------|----------|-------------|
195
+ | Environment variables | Highest | `INSIM_LOGIN`, `INSIM_ACCESS_KEY` |
196
+ | Credentials file | Lower | `~/.insim/credentials.json` (created by `insim login`) |
197
+
198
+ Custom server URL:
199
+ ```bash
200
+ export INSIM_BASE_URL="https://custom-server.com"
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ git clone https://github.com/ArdaryinSIM/insim-cli.git
209
+ cd insim-cli
210
+ pip install -e ".[dev]"
211
+ pytest tests/ -v
212
+ ```
213
+
214
+ ---
215
+
216
+ ## About inSIM
217
+
218
+ [inSIM](https://insim.app) is a SaaS platform for SMS management, CRM, and marketing campaigns. It connects to your phone via a mobile app and lets you send, receive, and track SMS from your computer.
219
+
220
+ **insim-cli** is the official command-line interface, built by [Reach Technologies SAS](https://reach-technologies.com).
221
+
222
+ ---
223
+
224
+ ## License
225
+
226
+ MIT
@@ -0,0 +1,2 @@
1
+ """inSIM CLI — Control your SMS, contacts and campaigns from the command line."""
2
+ __version__ = "1.0.0"
File without changes
@@ -0,0 +1,67 @@
1
+ """Account info and configuration."""
2
+ import click
3
+ from cli_anything.insim.core.api import InsimAPIError
4
+ from cli_anything.insim.core.auth import require_auth
5
+ from cli_anything.insim.utils.output import output, print_error
6
+
7
+
8
+ @click.group()
9
+ def account():
10
+ """Account info and configuration."""
11
+ pass
12
+
13
+
14
+ @account.command("info")
15
+ def account_info():
16
+ """Show account information: login, plan, credits, options."""
17
+ ctx = click.get_current_context()
18
+ json_mode = ctx.obj.get("json", False) if ctx.obj else False
19
+ try:
20
+ api = require_auth()
21
+ result = api.post("/api/v2/account")
22
+ if json_mode:
23
+ output(result, json_mode=True)
24
+ else:
25
+ acct = result.get("account", {})
26
+ click.echo(click.style("Account Information", bold=True))
27
+ click.echo(f" {click.style('Login', fg='cyan')}: {acct.get('login', '')}")
28
+ click.echo(f" {click.style('Plan', fg='cyan')}: {acct.get('plan', '')}")
29
+ click.echo(f" {click.style('Credits', fg='cyan')}: {acct.get('credits', '')}")
30
+ options = acct.get("options", {})
31
+ if options:
32
+ click.echo(f" {click.style('Options', fg='cyan')}:")
33
+ for key, val in options.items():
34
+ click.echo(f" {key}: {val}")
35
+ except InsimAPIError as e:
36
+ print_error(e.error_code, str(e), e.field, e.extra)
37
+ raise SystemExit(1)
38
+
39
+
40
+ @account.command("webhooks-set")
41
+ @click.option("--sms", default="", help="Incoming SMS webhook URL.")
42
+ @click.option("--dlr", default="", help="Delivery status webhook URL.")
43
+ @click.option("--clicks", default="", help="Link clicks webhook URL.")
44
+ @click.option("--calls", default="", help="Call events webhook URL.")
45
+ @click.option("--qualif", default="", help="Call qualifications webhook URL.")
46
+ def account_webhooks_set(sms, dlr, clicks, calls, qualif):
47
+ """Set account webhook URLs."""
48
+ ctx = click.get_current_context()
49
+ json_mode = ctx.obj.get("json", False) if ctx.obj else False
50
+ try:
51
+ api = require_auth()
52
+ result = api.post("/api/v2/account/webhooks", {
53
+ "webhooks": {
54
+ "incoming_sms": sms,
55
+ "delivery_status": dlr,
56
+ "link_clicks": clicks,
57
+ "call_events": calls,
58
+ "call_qualifications": qualif,
59
+ },
60
+ })
61
+ if json_mode:
62
+ output(result, json_mode=True)
63
+ else:
64
+ click.echo(click.style("Webhooks updated.", fg="green"))
65
+ except InsimAPIError as e:
66
+ print_error(e.error_code, str(e), e.field, e.extra)
67
+ raise SystemExit(1)
@@ -0,0 +1,68 @@
1
+ """HTTP client for inSIM API v2."""
2
+ import requests
3
+ from typing import Any
4
+
5
+
6
+ class InsimAPIError(Exception):
7
+ """Error returned by the inSIM API."""
8
+ def __init__(self, message: str, error_code: str = "", status: int = 0, field: str = "", extra: dict | None = None):
9
+ self.error_code = error_code
10
+ self.status = status
11
+ self.field = field
12
+ self.extra = extra or {}
13
+ super().__init__(message)
14
+
15
+
16
+ class InsimAPI:
17
+ """Client for inSIM API v2."""
18
+
19
+ def __init__(self, base_url: str, login: str, access_key: str):
20
+ self.base_url = base_url.rstrip("/")
21
+ self.login = login
22
+ self.access_key = access_key
23
+ self.session = requests.Session()
24
+ self.session.headers.update({
25
+ "Content-Type": "application/json",
26
+ "User-Agent": "insim-cli/1.0.0",
27
+ })
28
+
29
+ def post(self, endpoint: str, data: dict | None = None) -> dict:
30
+ """POST to an API v2 endpoint with automatic auth injection."""
31
+ payload = {
32
+ "login": self.login,
33
+ "accessKey": self.access_key,
34
+ }
35
+ if data:
36
+ payload.update(data)
37
+
38
+ url = f"{self.base_url}{endpoint}"
39
+
40
+ try:
41
+ resp = self.session.post(url, json=payload, timeout=60, verify=False)
42
+ except requests.ConnectionError:
43
+ raise InsimAPIError(f"Cannot connect to {self.base_url}. Is the server running?")
44
+ except requests.Timeout:
45
+ raise InsimAPIError("Request timed out after 30 seconds.")
46
+
47
+ try:
48
+ body = resp.json()
49
+ except ValueError:
50
+ raise InsimAPIError(f"Invalid response from server (HTTP {resp.status_code})")
51
+
52
+ if not body.get("success", False):
53
+ extra = {}
54
+ if body.get("error_code") == "LICENSE_REQUIRED":
55
+ extra = {
56
+ "subscription_type": body.get("subscription_type", ""),
57
+ "upgrade_url": body.get("upgrade_url", ""),
58
+ "feature": body.get("feature", ""),
59
+ }
60
+ raise InsimAPIError(
61
+ message=body.get("error", "Unknown error"),
62
+ error_code=body.get("error_code", "UNKNOWN"),
63
+ status=resp.status_code,
64
+ field=body.get("field", ""),
65
+ extra=extra,
66
+ )
67
+
68
+ return body
@@ -0,0 +1,74 @@
1
+ """Authentication: login, logout, credentials storage."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from cli_anything.insim.core.api import InsimAPI, InsimAPIError
7
+
8
+ CREDENTIALS_DIR = Path.home() / ".insim"
9
+ CREDENTIALS_FILE = CREDENTIALS_DIR / "credentials.json"
10
+ DEFAULT_BASE_URL = "https://www.insim.app"
11
+
12
+
13
+ def get_base_url() -> str:
14
+ return os.environ.get("INSIM_BASE_URL", DEFAULT_BASE_URL)
15
+
16
+
17
+ def save_credentials(login: str, access_key: str, base_url: str = "") -> None:
18
+ CREDENTIALS_DIR.mkdir(parents=True, exist_ok=True)
19
+ data = {"login": login, "accessKey": access_key}
20
+ if base_url:
21
+ data["base_url"] = base_url
22
+ CREDENTIALS_FILE.write_text(json.dumps(data, indent=2))
23
+
24
+
25
+ def load_credentials() -> Optional[dict]:
26
+ # Priority 1: environment variables
27
+ env_login = os.environ.get("INSIM_LOGIN")
28
+ env_key = os.environ.get("INSIM_ACCESS_KEY")
29
+ if env_login and env_key:
30
+ return {"login": env_login, "accessKey": env_key}
31
+
32
+ # Priority 2: credentials file
33
+ if CREDENTIALS_FILE.exists():
34
+ try:
35
+ return json.loads(CREDENTIALS_FILE.read_text())
36
+ except (json.JSONDecodeError, OSError):
37
+ return None
38
+
39
+ return None
40
+
41
+
42
+ def remove_credentials() -> bool:
43
+ if CREDENTIALS_FILE.exists():
44
+ CREDENTIALS_FILE.unlink()
45
+ return True
46
+ return False
47
+
48
+
49
+ def login(email: str, access_key: str) -> dict:
50
+ """Validate credentials against the API and save them."""
51
+ base_url = get_base_url()
52
+ api = InsimAPI(base_url, email, access_key)
53
+ result = api.post("/api/v2/account")
54
+ save_credentials(email, access_key, base_url)
55
+ return result.get("account", {})
56
+
57
+
58
+ def require_auth() -> InsimAPI:
59
+ """Return an authenticated API client or raise an error."""
60
+ creds = load_credentials()
61
+ if not creds:
62
+ raise InsimAPIError(
63
+ "Not logged in. Run: insim login YOUR_EMAIL --key YOUR_ACCESS_KEY",
64
+ error_code="NOT_AUTHENTICATED",
65
+ )
66
+ base_url = creds.get("base_url", get_base_url())
67
+ return InsimAPI(base_url, creds["login"], creds["accessKey"])
68
+
69
+
70
+ def whoami() -> dict:
71
+ """Return account info for the current user."""
72
+ api = require_auth()
73
+ result = api.post("/api/v2/account")
74
+ return result.get("account", {})