codicent-cli 0.7.1__tar.gz → 0.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codicent-cli
3
- Version: 0.7.1
3
+ Version: 0.8.0
4
4
  Summary: Command-line interface for the Codicent API
5
5
  Home-page: https://github.com/izaxon/codicent-cli
6
6
  Author: Johan Isaksson
@@ -27,6 +27,7 @@ Requires-Dist: rich
27
27
  Requires-Dist: codicent-py
28
28
  Requires-Dist: prompt_toolkit
29
29
  Requires-Dist: requests
30
+ Requires-Dist: pyyaml
30
31
  Dynamic: author
31
32
  Dynamic: author-email
32
33
  Dynamic: classifier
@@ -104,6 +105,28 @@ pip install git+https://github.com/izaxon/codicent-cli.git@v0.4.8
104
105
  export CODICENT_TOKEN="YOUR_API_TOKEN"
105
106
  ```
106
107
 
108
+ Or use the built-in device-flow authentication:
109
+ ```bash
110
+ codicent auth # stores token globally in ~/.codicent_token
111
+ codicent auth --local # stores token in .codicent_token in the current directory
112
+ ```
113
+
114
+ ### Per-folder Projects
115
+
116
+ You can have a different Codicent project per directory by storing a local token:
117
+
118
+ ```bash
119
+ cd ~/work/projectA
120
+ codicent auth --local # authenticates and saves token to ~/work/projectA/.codicent_token
121
+
122
+ cd ~/work/projectB
123
+ codicent auth --local # authenticates and saves token to ~/work/projectB/.codicent_token
124
+ ```
125
+
126
+ The CLI always checks the current directory first, then falls back to the global `~/.codicent_token`, and finally to the `CODICENT_TOKEN` environment variable.
127
+
128
+ > **Tip**: Add `.codicent_token` to your `.gitignore` to avoid accidentally committing tokens.
129
+
107
130
  ### Command Options
108
131
 
109
132
  ```
@@ -211,6 +234,102 @@ The CLI provides helpful error messages for common issues:
211
234
  - **Input validation**: Prevents empty or overly long inputs
212
235
  - **Keyboard interrupts**: Clean exit handling
213
236
 
237
+ ## Project Init
238
+
239
+ Use `codicent init` to instantly set up a new project from a template. Each template posts a ready-to-use set of AI instructions, configuration, automation rules, and sample data.
240
+
241
+ ```bash
242
+ codicent init --list # list templates
243
+ codicent init # use default template
244
+ codicent init --template koi-studio # CRM pipeline
245
+ codicent init --template document-processing # document intake
246
+ codicent init --template koi-studio --dry-run # preview without posting
247
+ codicent init --force # re-init even if already set up
248
+ ```
249
+
250
+ ### Available templates
251
+
252
+ | Template | Description |
253
+ |---|---|
254
+ | `default` | General-purpose: task workflow, auto-summarize notes via `#transform` |
255
+ | `koi-studio` | CRM: automatic lead extraction from raw text, follow-up email drafting |
256
+ | `document-processing` | Document intake: extract text, structure key fields with AI, review workflow |
257
+
258
+ ### How it works
259
+
260
+ Templates are JSON files in `templates/`. Each defines:
261
+ - **`variables`** — prompted at init time (e.g. company name). Values with defaults are applied silently; values without defaults require manual entry.
262
+ - **`messages`** — posted in order. Each has an optional `label` shown in progress output.
263
+
264
+ Key message types posted by templates:
265
+ - `#instructions` — sets the AI assistant's persona and focus
266
+ - `#appconfig #data` — configures AI model, temperature, and token limit
267
+ - `#process #data` — defines pipeline stages (shown as kanban columns in the UI)
268
+ - `#transform #data` — automation rule: when a message with tag X arrives, run AI and produce a message with tag Y
269
+ - sample `#data` messages — so lists aren't empty on first login
270
+
271
+ ### `--dry-run` mode
272
+
273
+ Prints every message that *would* be posted, with its label and a content preview, without any API calls or auth required. Useful for reviewing a template before committing.
274
+
275
+ ## Wrap — AI CLI Remote Chat Bridge
276
+
277
+ Use `codicent wrap` to bridge a local AI CLI tool (`claude`, `codex`, `opencode`, etc.) to your codicent project chat. Remote users and the Codicent AI can interact with the locally running tool in real time through the existing codicent web UI.
278
+
279
+ ```bash
280
+ codicent wrap claude # wrap Claude Code
281
+ codicent wrap opencode # wrap OpenCode
282
+ codicent wrap --label "Refactor sprint" codex # named session
283
+ codicent wrap --poll-interval 5 claude # slower polling
284
+ codicent wrap --binary /opt/ai/claude claude # explicit binary path
285
+ ```
286
+
287
+ ### How it works
288
+
289
+ 1. Starts the specified tool as a local subprocess with full stdin/stdout control.
290
+ 2. Opens a **session** tagged `#wrap #wrap_<id>` in your codicent project.
291
+ 3. **Output** from the tool is captured and posted to codicent tagged `#wrap #wrap_<id> #output`.
292
+ 4. **Input** is received by polling for messages tagged `#wrap #wrap_<id> #input` — any message posted remotely with those tags is forwarded to the tool's stdin.
293
+
294
+ The session ID is printed on startup. To send text to the running tool from the codicent web UI, post a message with `#wrap_<id> #input` in the content.
295
+
296
+ ### Startup output
297
+
298
+ ```
299
+ 🔗 Wrap session starting…
300
+ Session ID: wrap_a3f7c1b2
301
+ Project: myproject
302
+ Tool: claude (/usr/local/bin/claude)
303
+ Remote tag: #wrap_a3f7c1b2
304
+ Post messages tagged #wrap_a3f7c1b2 #input to send text to the tool.
305
+ Press Ctrl+C to stop the session.
306
+ ```
307
+
308
+ ### Configuration (`~/.codicent/wrap-config.yaml`)
309
+
310
+ ```yaml
311
+ poll_interval: 2 # seconds between input polls
312
+ output_batch_ms: 500 # ms to batch output before posting
313
+
314
+ tools:
315
+ claude:
316
+ binary: claude # binary name or full path
317
+ codex:
318
+ binary: codex
319
+ opencode:
320
+ binary: opencode
321
+
322
+ auto_responses:
323
+ - pattern: "Do you want to continue"
324
+ reply: "y"
325
+ - pattern: "\\(y/n\\)"
326
+ reply: "y"
327
+ ```
328
+
329
+ Auto-response rules are matched against subprocess output using regex. When matched, the configured reply is written to stdin automatically (and posted to codicent for visibility).
330
+
331
+ If no config file exists, defaults are used: tools are resolved by name on `$PATH`, poll interval is 2 seconds.
332
+
214
333
  ## Development
215
334
 
216
335
  ### Running Tests
@@ -222,6 +341,7 @@ python -m pytest test_app.py -v
222
341
  ### Project Structure
223
342
 
224
343
  - `app.py` - Main application logic (single-file architecture)
344
+ - `wrap.py` - Wrap command: AI CLI remote chat bridge
225
345
  - `test_app.py` - Comprehensive test suite
226
346
  - `setup.py` - Package configuration
227
347
  - `requirements.txt` - Dependencies (now uses PyPI packages)
@@ -230,6 +350,7 @@ python -m pytest test_app.py -v
230
350
 
231
351
  - **codicent-py**: Core API client for Codicent services (now available on PyPI)
232
352
  - **rich**: Terminal formatting, markdown rendering, and animations
353
+ - **pyyaml**: YAML config file parsing for the wrap command
233
354
 
234
355
  ## Troubleshooting
235
356
 
@@ -62,6 +62,28 @@ pip install git+https://github.com/izaxon/codicent-cli.git@v0.4.8
62
62
  export CODICENT_TOKEN="YOUR_API_TOKEN"
63
63
  ```
64
64
 
65
+ Or use the built-in device-flow authentication:
66
+ ```bash
67
+ codicent auth # stores token globally in ~/.codicent_token
68
+ codicent auth --local # stores token in .codicent_token in the current directory
69
+ ```
70
+
71
+ ### Per-folder Projects
72
+
73
+ You can have a different Codicent project per directory by storing a local token:
74
+
75
+ ```bash
76
+ cd ~/work/projectA
77
+ codicent auth --local # authenticates and saves token to ~/work/projectA/.codicent_token
78
+
79
+ cd ~/work/projectB
80
+ codicent auth --local # authenticates and saves token to ~/work/projectB/.codicent_token
81
+ ```
82
+
83
+ The CLI always checks the current directory first, then falls back to the global `~/.codicent_token`, and finally to the `CODICENT_TOKEN` environment variable.
84
+
85
+ > **Tip**: Add `.codicent_token` to your `.gitignore` to avoid accidentally committing tokens.
86
+
65
87
  ### Command Options
66
88
 
67
89
  ```
@@ -169,6 +191,102 @@ The CLI provides helpful error messages for common issues:
169
191
  - **Input validation**: Prevents empty or overly long inputs
170
192
  - **Keyboard interrupts**: Clean exit handling
171
193
 
194
+ ## Project Init
195
+
196
+ Use `codicent init` to instantly set up a new project from a template. Each template posts a ready-to-use set of AI instructions, configuration, automation rules, and sample data.
197
+
198
+ ```bash
199
+ codicent init --list # list templates
200
+ codicent init # use default template
201
+ codicent init --template koi-studio # CRM pipeline
202
+ codicent init --template document-processing # document intake
203
+ codicent init --template koi-studio --dry-run # preview without posting
204
+ codicent init --force # re-init even if already set up
205
+ ```
206
+
207
+ ### Available templates
208
+
209
+ | Template | Description |
210
+ |---|---|
211
+ | `default` | General-purpose: task workflow, auto-summarize notes via `#transform` |
212
+ | `koi-studio` | CRM: automatic lead extraction from raw text, follow-up email drafting |
213
+ | `document-processing` | Document intake: extract text, structure key fields with AI, review workflow |
214
+
215
+ ### How it works
216
+
217
+ Templates are JSON files in `templates/`. Each defines:
218
+ - **`variables`** — prompted at init time (e.g. company name). Values with defaults are applied silently; values without defaults require manual entry.
219
+ - **`messages`** — posted in order. Each has an optional `label` shown in progress output.
220
+
221
+ Key message types posted by templates:
222
+ - `#instructions` — sets the AI assistant's persona and focus
223
+ - `#appconfig #data` — configures AI model, temperature, and token limit
224
+ - `#process #data` — defines pipeline stages (shown as kanban columns in the UI)
225
+ - `#transform #data` — automation rule: when a message with tag X arrives, run AI and produce a message with tag Y
226
+ - sample `#data` messages — so lists aren't empty on first login
227
+
228
+ ### `--dry-run` mode
229
+
230
+ Prints every message that *would* be posted, with its label and a content preview, without any API calls or auth required. Useful for reviewing a template before committing.
231
+
232
+ ## Wrap — AI CLI Remote Chat Bridge
233
+
234
+ Use `codicent wrap` to bridge a local AI CLI tool (`claude`, `codex`, `opencode`, etc.) to your codicent project chat. Remote users and the Codicent AI can interact with the locally running tool in real time through the existing codicent web UI.
235
+
236
+ ```bash
237
+ codicent wrap claude # wrap Claude Code
238
+ codicent wrap opencode # wrap OpenCode
239
+ codicent wrap --label "Refactor sprint" codex # named session
240
+ codicent wrap --poll-interval 5 claude # slower polling
241
+ codicent wrap --binary /opt/ai/claude claude # explicit binary path
242
+ ```
243
+
244
+ ### How it works
245
+
246
+ 1. Starts the specified tool as a local subprocess with full stdin/stdout control.
247
+ 2. Opens a **session** tagged `#wrap #wrap_<id>` in your codicent project.
248
+ 3. **Output** from the tool is captured and posted to codicent tagged `#wrap #wrap_<id> #output`.
249
+ 4. **Input** is received by polling for messages tagged `#wrap #wrap_<id> #input` — any message posted remotely with those tags is forwarded to the tool's stdin.
250
+
251
+ The session ID is printed on startup. To send text to the running tool from the codicent web UI, post a message with `#wrap_<id> #input` in the content.
252
+
253
+ ### Startup output
254
+
255
+ ```
256
+ 🔗 Wrap session starting…
257
+ Session ID: wrap_a3f7c1b2
258
+ Project: myproject
259
+ Tool: claude (/usr/local/bin/claude)
260
+ Remote tag: #wrap_a3f7c1b2
261
+ Post messages tagged #wrap_a3f7c1b2 #input to send text to the tool.
262
+ Press Ctrl+C to stop the session.
263
+ ```
264
+
265
+ ### Configuration (`~/.codicent/wrap-config.yaml`)
266
+
267
+ ```yaml
268
+ poll_interval: 2 # seconds between input polls
269
+ output_batch_ms: 500 # ms to batch output before posting
270
+
271
+ tools:
272
+ claude:
273
+ binary: claude # binary name or full path
274
+ codex:
275
+ binary: codex
276
+ opencode:
277
+ binary: opencode
278
+
279
+ auto_responses:
280
+ - pattern: "Do you want to continue"
281
+ reply: "y"
282
+ - pattern: "\\(y/n\\)"
283
+ reply: "y"
284
+ ```
285
+
286
+ Auto-response rules are matched against subprocess output using regex. When matched, the configured reply is written to stdin automatically (and posted to codicent for visibility).
287
+
288
+ If no config file exists, defaults are used: tools are resolved by name on `$PATH`, poll interval is 2 seconds.
289
+
172
290
  ## Development
173
291
 
174
292
  ### Running Tests
@@ -180,6 +298,7 @@ python -m pytest test_app.py -v
180
298
  ### Project Structure
181
299
 
182
300
  - `app.py` - Main application logic (single-file architecture)
301
+ - `wrap.py` - Wrap command: AI CLI remote chat bridge
183
302
  - `test_app.py` - Comprehensive test suite
184
303
  - `setup.py` - Package configuration
185
304
  - `requirements.txt` - Dependencies (now uses PyPI packages)
@@ -188,6 +307,7 @@ python -m pytest test_app.py -v
188
307
 
189
308
  - **codicent-py**: Core API client for Codicent services (now available on PyPI)
190
309
  - **rich**: Terminal formatting, markdown rendering, and animations
310
+ - **pyyaml**: YAML config file parsing for the wrap command
191
311
 
192
312
  ## Troubleshooting
193
313
 
@@ -40,8 +40,10 @@ OPTIONS:
40
40
 
41
41
  AUTHENTICATION:
42
42
  auth Authenticate using device flow (project selected during web auth)
43
- logout Clear stored authentication
44
- status Check authentication status
43
+ auth --local Authenticate and store token in the current directory (.codicent_token)
44
+ logout Clear stored global authentication
45
+ logout --local Clear only the local (.codicent_token) token in the current directory
46
+ status Check authentication status (shows whether local or global token is used)
45
47
 
46
48
  FILE UPLOAD:
47
49
  upload Upload files and post messages with tags
@@ -55,13 +57,54 @@ FILE UPLOAD:
55
57
  PROJECT INIT:
56
58
  init Initialize current project with a template
57
59
 
58
- --list List available templates
60
+ --list List available templates with descriptions
59
61
  --template NAME Use a specific template (default: 'default')
62
+ --dry-run Preview messages without posting (no auth needed)
63
+ --force Re-initialize even if project already has #instructions
64
+
65
+ Templates:
66
+ default General-purpose: task workflow + auto-summarize notes
67
+ koi-studio CRM: lead extraction + follow-up email drafting pipeline
68
+ document-processing Document intake: extract, structure, and review documents
60
69
 
61
70
  Examples:
62
71
  codicent init
63
72
  codicent init --template koi-studio
73
+ codicent init --template document-processing --dry-run
64
74
  codicent init --list
75
+ codicent init --force
76
+
77
+ WRAP (AI CLI Remote Bridge):
78
+ wrap Bridge a local AI CLI tool to a remote codicent chat session.
79
+ Remote users and AI can interact with the locally running tool.
80
+
81
+ <tool> Tool to wrap: claude, codex, opencode, or any CLI tool
82
+ [tool-args...] Arguments passed through to the wrapped tool
83
+ --label TEXT Session label shown in codicent UI (default: "<tool> session")
84
+ --binary PATH Explicit path to the tool binary
85
+ --poll-interval N Seconds between input polls (default: 2)
86
+
87
+ How it works:
88
+ 1. Starts the tool as a local subprocess
89
+ 2. Posts tool output to codicent tagged #wrap #wrap_<id> #output
90
+ 3. Polls for messages tagged #wrap #wrap_<id> #input and writes them to stdin
91
+ 4. Remote users tag messages with #wrap_<id> #input to send text to the tool
92
+
93
+ Config file (~/.codicent/wrap-config.yaml):
94
+ poll_interval: 2
95
+ output_batch_ms: 500
96
+ tools:
97
+ claude:
98
+ binary: claude
99
+ auto_responses:
100
+ - pattern: "Do you want to continue"
101
+ reply: "y"
102
+
103
+ Examples:
104
+ codicent wrap claude
105
+ codicent wrap --label "Refactor session" opencode
106
+ codicent wrap --poll-interval 5 codex
107
+ codicent wrap --binary /usr/local/bin/claude claude --model claude-opus-4-5
65
108
 
66
109
  EXAMPLES:
67
110
  codicent auth
@@ -70,6 +113,7 @@ EXAMPLES:
70
113
  codicent "@mention Hello there"
71
114
  codicent upload document.txt *.log
72
115
  codicent init --template koi-studio
116
+ codicent wrap claude
73
117
  echo "Help me debug this" | codicent
74
118
 
75
119
  ENVIRONMENT:
@@ -81,7 +125,7 @@ For more information, visit: https://github.com/izaxon/codicent-cli
81
125
 
82
126
  def show_version():
83
127
  """Display version information."""
84
- print("Codicent CLI v0.7.1")
128
+ print("Codicent CLI v0.8.0")
85
129
 
86
130
  def validate_input(question):
87
131
  """Validate user input."""
@@ -115,6 +159,11 @@ def main():
115
159
  logging.getLogger().setLevel(logging.ERROR)
116
160
  sys.argv.remove("--quiet")
117
161
 
162
+ # Parse --local flag (store/remove token in current working directory)
163
+ use_local = "--local" in sys.argv
164
+ if use_local:
165
+ sys.argv.remove("--local")
166
+
118
167
  # Initialize auth handler
119
168
  auth = CodicentAuth()
120
169
 
@@ -123,8 +172,7 @@ def main():
123
172
  command = sys.argv[1]
124
173
 
125
174
  if command == "auth":
126
- # No project parameter needed - user selects project during web authorization
127
- token = auth.get_token(force_reauth=True)
175
+ token = auth.get_token(force_reauth=True, local=use_local)
128
176
  if token:
129
177
  console.print("[green]✅ Authentication successful![/green]")
130
178
  return 0
@@ -133,13 +181,13 @@ def main():
133
181
  return 1
134
182
 
135
183
  elif command == "logout":
136
- auth.logout()
184
+ auth.logout(local=use_local)
137
185
  return 0
138
186
 
139
187
  elif command == "status":
140
- token = auth.get_cached_token()
141
- if token:
142
- console.print("[green]✅ Authenticated (cached token available)[/green]")
188
+ source = auth.get_token_source()
189
+ if source:
190
+ console.print(f"[green]✅ Authenticated[/green] [dim]({source})[/dim]")
143
191
  return 0
144
192
  else:
145
193
  env_token = os.getenv("CODICENT_TOKEN")
@@ -270,6 +318,9 @@ def main():
270
318
  if len(sys.argv) > 1 and sys.argv[1] == "init":
271
319
  templates_dir = os.path.join(os.path.dirname(__file__), "templates")
272
320
 
321
+ dry_run = "--dry-run" in sys.argv
322
+ force = "--force" in sys.argv
323
+
273
324
  # --list: show available templates
274
325
  if "--list" in sys.argv:
275
326
  try:
@@ -279,7 +330,10 @@ def main():
279
330
  template_path = os.path.join(templates_dir, f"{name}.json")
280
331
  with open(template_path, encoding="utf-8") as fh:
281
332
  meta = json.load(fh)
282
- console.print(f" [bold bright_cyan]{name}[/bold bright_cyan] — {meta.get('description', '')}")
333
+ vars_info = ""
334
+ if meta.get("variables"):
335
+ vars_info = f" [dim]({len(meta['variables'])} variable(s))[/dim]"
336
+ console.print(f" [bold bright_cyan]{name}[/bold bright_cyan]{vars_info} — {meta.get('description', '')}")
283
337
  except Exception as e:
284
338
  console.print(f"[red]❌ Error listing templates: {e}[/red]")
285
339
  return 0
@@ -299,6 +353,51 @@ def main():
299
353
  console.print(f"[red]❌ Template '{template_name}' not found. Use 'codicent init --list' to see options.[/red]")
300
354
  return 1
301
355
 
356
+ with open(template_path, encoding="utf-8") as fh:
357
+ template = json.load(fh)
358
+
359
+ # Resolve variables: prompt for any without defaults
360
+ variable_values = {}
361
+ template_variables = template.get("variables", {})
362
+ for var_name, var_meta in template_variables.items():
363
+ default = var_meta.get("default", "")
364
+ description = var_meta.get("description", var_name)
365
+ if dry_run:
366
+ # In dry-run, use defaults without prompting
367
+ variable_values[var_name] = default or f"{{{var_name}}}"
368
+ elif default:
369
+ variable_values[var_name] = default
370
+ console.print(f"[dim] {var_name}: {default} (default)[/dim]")
371
+ else:
372
+ value = input(f" {description}: ").strip()
373
+ if not value:
374
+ console.print(f"[red]❌ Variable '{var_name}' is required.[/red]")
375
+ return 1
376
+ variable_values[var_name] = value
377
+
378
+ def apply_variables(text, project):
379
+ result = text.replace("{project}", project)
380
+ for var_name, var_value in variable_values.items():
381
+ result = result.replace("{" + var_name + "}", var_value)
382
+ return result
383
+
384
+ # --- Dry-run mode: print plan without calling API ---
385
+ if dry_run:
386
+ messages = template.get("messages", [])
387
+ console.print(f"\n[bold]Dry run — template [bright_cyan]{template_name}[/bright_cyan] ({len(messages)} messages)[/bold]")
388
+ console.print(f"[dim]No messages will be posted. Remove --dry-run to execute.[/dim]\n")
389
+ for i, entry in enumerate(messages, 1):
390
+ label = entry.get("label", f"Message {i}")
391
+ content = apply_variables(entry["content"], "{project}")
392
+ # Show first 120 chars of content
393
+ preview = content.replace("\n", " ↵ ")
394
+ if len(preview) > 120:
395
+ preview = preview[:117] + "..."
396
+ console.print(f" [bold]{i}.[/bold] [bright_cyan]{label}[/bright_cyan]")
397
+ console.print(f" [dim]{preview}[/dim]")
398
+ console.print()
399
+ return 0
400
+
302
401
  # Get authentication token
303
402
  token = auth.get_cached_token()
304
403
  if not token:
@@ -325,23 +424,41 @@ def main():
325
424
  console.print(f"[red]❌ Failed to initialize Codicent API client: {e}[/red]")
326
425
  return 1
327
426
 
328
- with open(template_path, encoding="utf-8") as fh:
329
- template = json.load(fh)
427
+ # Idempotency check: warn if #instructions already exists in this project
428
+ if not force:
429
+ try:
430
+ headers = {"Authorization": f"Bearer {token}"}
431
+ check_url = f"{codicent.base_url}api/GetMessages"
432
+ resp = requests.get(
433
+ check_url,
434
+ params={"mention": project, "tags": "instructions", "length": "1"},
435
+ headers=headers,
436
+ verify=codicent.verify_https,
437
+ timeout=10,
438
+ )
439
+ if resp.ok and resp.json():
440
+ console.print(f"[yellow]⚠ Project [bold]{project}[/bold] already has #instructions — it may have been initialized before.[/yellow]")
441
+ console.print("[dim]Pass --force to initialize anyway, or --dry-run to preview what would be posted.[/dim]")
442
+ return 1
443
+ except Exception:
444
+ logger.debug("Idempotency check failed (skipping)")
330
445
 
331
446
  messages = template.get("messages", [])
332
447
  console.print(f"Initializing [bold]{project}[/bold] with template [bold bright_cyan]{template_name}[/bold bright_cyan] ({len(messages)} messages)...")
333
448
 
334
449
  posted = 0
335
450
  failed = 0
336
- for entry in messages:
337
- content = entry["content"].replace("{project}", project)
451
+ for i, entry in enumerate(messages, 1):
452
+ label = entry.get("label", f"Message {i}")
453
+ content = apply_variables(entry["content"], project)
338
454
  msg_type = entry.get("type", "info")
339
455
  try:
340
- with console.status(f"[dim]Posting message {posted + 1}/{len(messages)}...[/dim]", spinner="dots"):
456
+ with console.status(f"[dim]({i}/{len(messages)}) {label}...[/dim]", spinner="dots"):
341
457
  codicent.post_message(content, type=msg_type)
458
+ console.print(f" [green]✓[/green] [dim]{label}[/dim]")
342
459
  posted += 1
343
460
  except Exception as e:
344
- console.print(f"[red]❌ Failed to post message: {e}[/red]")
461
+ console.print(f" [red]✗ {label}: {e}[/red]")
345
462
  failed += 1
346
463
 
347
464
  console.print()
@@ -350,6 +467,38 @@ def main():
350
467
  console.print(f"[red]❌ {failed} message(s) failed.[/red]")
351
468
  return 0 if failed == 0 else 1
352
469
 
470
+ # Handle wrap command
471
+ if len(sys.argv) > 1 and sys.argv[1] == "wrap":
472
+ from wrap import run_wrap_command
473
+
474
+ wrap_token = auth.get_cached_token()
475
+ if not wrap_token:
476
+ wrap_token = os.getenv("CODICENT_TOKEN")
477
+ if not wrap_token:
478
+ console.print("[red]❌ No authentication found.[/red]")
479
+ console.print("[dim]Run 'codicent auth' to authenticate, or set CODICENT_TOKEN environment variable[/dim]")
480
+ return 1
481
+
482
+ # Extract project and base_url from JWT token
483
+ try:
484
+ wrap_payload = wrap_token.split(".")[1]
485
+ wrap_payload += '=' * (-len(wrap_payload) % 4)
486
+ wrap_jwt = json.loads(base64.urlsafe_b64decode(wrap_payload).decode('utf-8'))
487
+ wrap_project = wrap_jwt["project"]
488
+ wrap_base_url = wrap_jwt.get("baseUrl", "https://codicent.com/")
489
+ except Exception as e:
490
+ console.print(f"[red]❌ Failed to extract project from token: {e}[/red]")
491
+ return 1
492
+
493
+ # Pass everything after "wrap" as args
494
+ wrap_args = sys.argv[2:]
495
+ return run_wrap_command(
496
+ args=wrap_args,
497
+ token=wrap_token,
498
+ base_url=wrap_base_url,
499
+ project=wrap_project,
500
+ )
501
+
353
502
  # Get authentication token
354
503
  token = auth.get_cached_token()
355
504
  if not token:
@@ -578,7 +727,7 @@ def main():
578
727
 
579
728
  header = Text()
580
729
  header.append("Codicent CLI", style="bold bright_cyan")
581
- header.append(" v0.7.1\n", style="bright_cyan")
730
+ header.append(" v0.8.0\n", style="bright_cyan")
582
731
  header.append("Type your questions or use Ctrl+C to exit.\n", style="dim")
583
732
  header.append("\n")
584
733
  header.append("Tip: ", style="bold dim")