codicent-cli 0.7.1__tar.gz → 0.8.1__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.1
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,9 @@ Requires-Dist: rich
27
27
  Requires-Dist: codicent-py
28
28
  Requires-Dist: prompt_toolkit
29
29
  Requires-Dist: requests
30
+ Requires-Dist: pyyaml
31
+ Requires-Dist: packaging
32
+ Requires-Dist: pyte
30
33
  Dynamic: author
31
34
  Dynamic: author-email
32
35
  Dynamic: classifier
@@ -104,6 +107,28 @@ pip install git+https://github.com/izaxon/codicent-cli.git@v0.4.8
104
107
  export CODICENT_TOKEN="YOUR_API_TOKEN"
105
108
  ```
106
109
 
110
+ Or use the built-in device-flow authentication:
111
+ ```bash
112
+ codicent auth # stores token globally in ~/.codicent_token
113
+ codicent auth --local # stores token in .codicent_token in the current directory
114
+ ```
115
+
116
+ ### Per-folder Projects
117
+
118
+ You can have a different Codicent project per directory by storing a local token:
119
+
120
+ ```bash
121
+ cd ~/work/projectA
122
+ codicent auth --local # authenticates and saves token to ~/work/projectA/.codicent_token
123
+
124
+ cd ~/work/projectB
125
+ codicent auth --local # authenticates and saves token to ~/work/projectB/.codicent_token
126
+ ```
127
+
128
+ The CLI always checks the current directory first, then falls back to the global `~/.codicent_token`, and finally to the `CODICENT_TOKEN` environment variable.
129
+
130
+ > **Tip**: Add `.codicent_token` to your `.gitignore` to avoid accidentally committing tokens.
131
+
107
132
  ### Command Options
108
133
 
109
134
  ```
@@ -211,6 +236,102 @@ The CLI provides helpful error messages for common issues:
211
236
  - **Input validation**: Prevents empty or overly long inputs
212
237
  - **Keyboard interrupts**: Clean exit handling
213
238
 
239
+ ## Project Init
240
+
241
+ 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.
242
+
243
+ ```bash
244
+ codicent init --list # list templates
245
+ codicent init # use default template
246
+ codicent init --template koi-studio # CRM pipeline
247
+ codicent init --template document-processing # document intake
248
+ codicent init --template koi-studio --dry-run # preview without posting
249
+ codicent init --force # re-init even if already set up
250
+ ```
251
+
252
+ ### Available templates
253
+
254
+ | Template | Description |
255
+ |---|---|
256
+ | `default` | General-purpose: task workflow, auto-summarize notes via `#transform` |
257
+ | `koi-studio` | CRM: automatic lead extraction from raw text, follow-up email drafting |
258
+ | `document-processing` | Document intake: extract text, structure key fields with AI, review workflow |
259
+
260
+ ### How it works
261
+
262
+ Templates are JSON files in `templates/`. Each defines:
263
+ - **`variables`** — prompted at init time (e.g. company name). Values with defaults are applied silently; values without defaults require manual entry.
264
+ - **`messages`** — posted in order. Each has an optional `label` shown in progress output.
265
+
266
+ Key message types posted by templates:
267
+ - `#instructions` — sets the AI assistant's persona and focus
268
+ - `#appconfig #data` — configures AI model, temperature, and token limit
269
+ - `#process #data` — defines pipeline stages (shown as kanban columns in the UI)
270
+ - `#transform #data` — automation rule: when a message with tag X arrives, run AI and produce a message with tag Y
271
+ - sample `#data` messages — so lists aren't empty on first login
272
+
273
+ ### `--dry-run` mode
274
+
275
+ 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.
276
+
277
+ ## Wrap — AI CLI Remote Chat Bridge
278
+
279
+ 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.
280
+
281
+ ```bash
282
+ codicent wrap claude # wrap Claude Code
283
+ codicent wrap opencode # wrap OpenCode
284
+ codicent wrap --label "Refactor sprint" codex # named session
285
+ codicent wrap --poll-interval 5 claude # slower polling
286
+ codicent wrap --binary /opt/ai/claude claude # explicit binary path
287
+ ```
288
+
289
+ ### How it works
290
+
291
+ 1. Starts the specified tool as a local subprocess with full stdin/stdout control.
292
+ 2. Opens a **session** tagged `#wrap #wrap_<id>` in your codicent project.
293
+ 3. **Output** from the tool is captured and posted to codicent tagged `#wrap #wrap_<id> #output`.
294
+ 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.
295
+
296
+ 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.
297
+
298
+ ### Startup output
299
+
300
+ ```
301
+ 🔗 Wrap session starting…
302
+ Session ID: wrap_a3f7c1b2
303
+ Project: myproject
304
+ Tool: claude (/usr/local/bin/claude)
305
+ Remote tag: #wrap_a3f7c1b2
306
+ Post messages tagged #wrap_a3f7c1b2 #input to send text to the tool.
307
+ Press Ctrl+C to stop the session.
308
+ ```
309
+
310
+ ### Configuration (`~/.codicent/wrap-config.yaml`)
311
+
312
+ ```yaml
313
+ poll_interval: 2 # seconds between input polls
314
+ output_batch_ms: 500 # ms to batch output before posting
315
+
316
+ tools:
317
+ claude:
318
+ binary: claude # binary name or full path
319
+ codex:
320
+ binary: codex
321
+ opencode:
322
+ binary: opencode
323
+
324
+ auto_responses:
325
+ - pattern: "Do you want to continue"
326
+ reply: "y"
327
+ - pattern: "\\(y/n\\)"
328
+ reply: "y"
329
+ ```
330
+
331
+ 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).
332
+
333
+ If no config file exists, defaults are used: tools are resolved by name on `$PATH`, poll interval is 2 seconds.
334
+
214
335
  ## Development
215
336
 
216
337
  ### Running Tests
@@ -222,6 +343,7 @@ python -m pytest test_app.py -v
222
343
  ### Project Structure
223
344
 
224
345
  - `app.py` - Main application logic (single-file architecture)
346
+ - `wrap.py` - Wrap command: AI CLI remote chat bridge
225
347
  - `test_app.py` - Comprehensive test suite
226
348
  - `setup.py` - Package configuration
227
349
  - `requirements.txt` - Dependencies (now uses PyPI packages)
@@ -230,6 +352,7 @@ python -m pytest test_app.py -v
230
352
 
231
353
  - **codicent-py**: Core API client for Codicent services (now available on PyPI)
232
354
  - **rich**: Terminal formatting, markdown rendering, and animations
355
+ - **pyyaml**: YAML config file parsing for the wrap command
233
356
 
234
357
  ## Troubleshooting
235
358
 
@@ -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,8 +113,15 @@ 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
117
+ codicent update
118
+ codicent update --install
73
119
  echo "Help me debug this" | codicent
74
120
 
121
+ UPDATE:
122
+ update Check if a newer version is available on PyPI
123
+ --install Also upgrade immediately via pip
124
+
75
125
  ENVIRONMENT:
76
126
  CODICENT_TOKEN Your Codicent API token (fallback if no cached auth)
77
127
 
@@ -79,9 +129,11 @@ For more information, visit: https://github.com/izaxon/codicent-cli
79
129
  """
80
130
  print(help_text.strip())
81
131
 
132
+ CURRENT_VERSION = "0.8.1"
133
+
82
134
  def show_version():
83
135
  """Display version information."""
84
- print("Codicent CLI v0.7.1")
136
+ print(f"Codicent CLI v{CURRENT_VERSION}")
85
137
 
86
138
  def validate_input(question):
87
139
  """Validate user input."""
@@ -115,6 +167,11 @@ def main():
115
167
  logging.getLogger().setLevel(logging.ERROR)
116
168
  sys.argv.remove("--quiet")
117
169
 
170
+ # Parse --local flag (store/remove token in current working directory)
171
+ use_local = "--local" in sys.argv
172
+ if use_local:
173
+ sys.argv.remove("--local")
174
+
118
175
  # Initialize auth handler
119
176
  auth = CodicentAuth()
120
177
 
@@ -123,8 +180,7 @@ def main():
123
180
  command = sys.argv[1]
124
181
 
125
182
  if command == "auth":
126
- # No project parameter needed - user selects project during web authorization
127
- token = auth.get_token(force_reauth=True)
183
+ token = auth.get_token(force_reauth=True, local=use_local)
128
184
  if token:
129
185
  console.print("[green]✅ Authentication successful![/green]")
130
186
  return 0
@@ -133,13 +189,13 @@ def main():
133
189
  return 1
134
190
 
135
191
  elif command == "logout":
136
- auth.logout()
192
+ auth.logout(local=use_local)
137
193
  return 0
138
194
 
139
195
  elif command == "status":
140
- token = auth.get_cached_token()
141
- if token:
142
- console.print("[green]✅ Authenticated (cached token available)[/green]")
196
+ source = auth.get_token_source()
197
+ if source:
198
+ console.print(f"[green]✅ Authenticated[/green] [dim]({source})[/dim]")
143
199
  return 0
144
200
  else:
145
201
  env_token = os.getenv("CODICENT_TOKEN")
@@ -152,6 +208,54 @@ def main():
152
208
  console.print("[dim]Run 'codicent auth' to authenticate[/dim]")
153
209
  return 1
154
210
 
211
+ # Handle update command
212
+ if len(sys.argv) > 1 and sys.argv[1] == "update":
213
+ do_install = "--install" in sys.argv
214
+ try:
215
+ resp = requests.get(
216
+ "https://pypi.org/pypi/codicent-cli/json",
217
+ timeout=10,
218
+ )
219
+ resp.raise_for_status()
220
+ latest = resp.json()["info"]["version"]
221
+ except Exception as e:
222
+ console.print(f"[red]❌ Could not check PyPI: {e}[/red]")
223
+ return 1
224
+
225
+ from packaging.version import Version
226
+
227
+ # Detect whether we're running inside a pipx-managed environment
228
+ is_pipx = "pipx" in sys.executable.replace("\\", "/")
229
+
230
+ if Version(latest) > Version(CURRENT_VERSION):
231
+ console.print(
232
+ f"[yellow]⬆ New version available:[/yellow] "
233
+ f"[bold]{latest}[/bold] (you have {CURRENT_VERSION})"
234
+ )
235
+ if do_install:
236
+ if is_pipx:
237
+ console.print("[dim]Running pipx upgrade codicent-cli...[/dim]")
238
+ result = subprocess.run(
239
+ ["pipx", "upgrade", "codicent-cli"],
240
+ capture_output=False,
241
+ )
242
+ else:
243
+ console.print("[dim]Running pip install --upgrade codicent-cli...[/dim]")
244
+ result = subprocess.run(
245
+ [sys.executable, "-m", "pip", "install", "--upgrade", "codicent-cli"],
246
+ capture_output=False,
247
+ )
248
+ return result.returncode
249
+ else:
250
+ upgrade_cmd = "pipx upgrade codicent-cli" if is_pipx else "pip install --upgrade codicent-cli"
251
+ console.print(
252
+ f"[dim]Run [bold]codicent update --install[/bold] to upgrade, "
253
+ f"or [bold]{upgrade_cmd}[/bold][/dim]"
254
+ )
255
+ else:
256
+ console.print(f"[green]✅ Already up to date (v{CURRENT_VERSION})[/green]")
257
+ return 0
258
+
155
259
  # Handle upload command
156
260
  if len(sys.argv) > 1 and sys.argv[1] == "upload":
157
261
  if len(sys.argv) < 3:
@@ -270,6 +374,9 @@ def main():
270
374
  if len(sys.argv) > 1 and sys.argv[1] == "init":
271
375
  templates_dir = os.path.join(os.path.dirname(__file__), "templates")
272
376
 
377
+ dry_run = "--dry-run" in sys.argv
378
+ force = "--force" in sys.argv
379
+
273
380
  # --list: show available templates
274
381
  if "--list" in sys.argv:
275
382
  try:
@@ -279,7 +386,10 @@ def main():
279
386
  template_path = os.path.join(templates_dir, f"{name}.json")
280
387
  with open(template_path, encoding="utf-8") as fh:
281
388
  meta = json.load(fh)
282
- console.print(f" [bold bright_cyan]{name}[/bold bright_cyan] — {meta.get('description', '')}")
389
+ vars_info = ""
390
+ if meta.get("variables"):
391
+ vars_info = f" [dim]({len(meta['variables'])} variable(s))[/dim]"
392
+ console.print(f" [bold bright_cyan]{name}[/bold bright_cyan]{vars_info} — {meta.get('description', '')}")
283
393
  except Exception as e:
284
394
  console.print(f"[red]❌ Error listing templates: {e}[/red]")
285
395
  return 0
@@ -299,6 +409,51 @@ def main():
299
409
  console.print(f"[red]❌ Template '{template_name}' not found. Use 'codicent init --list' to see options.[/red]")
300
410
  return 1
301
411
 
412
+ with open(template_path, encoding="utf-8") as fh:
413
+ template = json.load(fh)
414
+
415
+ # Resolve variables: prompt for any without defaults
416
+ variable_values = {}
417
+ template_variables = template.get("variables", {})
418
+ for var_name, var_meta in template_variables.items():
419
+ default = var_meta.get("default", "")
420
+ description = var_meta.get("description", var_name)
421
+ if dry_run:
422
+ # In dry-run, use defaults without prompting
423
+ variable_values[var_name] = default or f"{{{var_name}}}"
424
+ elif default:
425
+ variable_values[var_name] = default
426
+ console.print(f"[dim] {var_name}: {default} (default)[/dim]")
427
+ else:
428
+ value = input(f" {description}: ").strip()
429
+ if not value:
430
+ console.print(f"[red]❌ Variable '{var_name}' is required.[/red]")
431
+ return 1
432
+ variable_values[var_name] = value
433
+
434
+ def apply_variables(text, project):
435
+ result = text.replace("{project}", project)
436
+ for var_name, var_value in variable_values.items():
437
+ result = result.replace("{" + var_name + "}", var_value)
438
+ return result
439
+
440
+ # --- Dry-run mode: print plan without calling API ---
441
+ if dry_run:
442
+ messages = template.get("messages", [])
443
+ console.print(f"\n[bold]Dry run — template [bright_cyan]{template_name}[/bright_cyan] ({len(messages)} messages)[/bold]")
444
+ console.print(f"[dim]No messages will be posted. Remove --dry-run to execute.[/dim]\n")
445
+ for i, entry in enumerate(messages, 1):
446
+ label = entry.get("label", f"Message {i}")
447
+ content = apply_variables(entry["content"], "{project}")
448
+ # Show first 120 chars of content
449
+ preview = content.replace("\n", " ↵ ")
450
+ if len(preview) > 120:
451
+ preview = preview[:117] + "..."
452
+ console.print(f" [bold]{i}.[/bold] [bright_cyan]{label}[/bright_cyan]")
453
+ console.print(f" [dim]{preview}[/dim]")
454
+ console.print()
455
+ return 0
456
+
302
457
  # Get authentication token
303
458
  token = auth.get_cached_token()
304
459
  if not token:
@@ -325,23 +480,41 @@ def main():
325
480
  console.print(f"[red]❌ Failed to initialize Codicent API client: {e}[/red]")
326
481
  return 1
327
482
 
328
- with open(template_path, encoding="utf-8") as fh:
329
- template = json.load(fh)
483
+ # Idempotency check: warn if #instructions already exists in this project
484
+ if not force:
485
+ try:
486
+ headers = {"Authorization": f"Bearer {token}"}
487
+ check_url = f"{codicent.base_url}api/GetMessages"
488
+ resp = requests.get(
489
+ check_url,
490
+ params={"mention": project, "tags": "instructions", "length": "1"},
491
+ headers=headers,
492
+ verify=codicent.verify_https,
493
+ timeout=10,
494
+ )
495
+ if resp.ok and resp.json():
496
+ console.print(f"[yellow]⚠ Project [bold]{project}[/bold] already has #instructions — it may have been initialized before.[/yellow]")
497
+ console.print("[dim]Pass --force to initialize anyway, or --dry-run to preview what would be posted.[/dim]")
498
+ return 1
499
+ except Exception:
500
+ logger.debug("Idempotency check failed (skipping)")
330
501
 
331
502
  messages = template.get("messages", [])
332
503
  console.print(f"Initializing [bold]{project}[/bold] with template [bold bright_cyan]{template_name}[/bold bright_cyan] ({len(messages)} messages)...")
333
504
 
334
505
  posted = 0
335
506
  failed = 0
336
- for entry in messages:
337
- content = entry["content"].replace("{project}", project)
507
+ for i, entry in enumerate(messages, 1):
508
+ label = entry.get("label", f"Message {i}")
509
+ content = apply_variables(entry["content"], project)
338
510
  msg_type = entry.get("type", "info")
339
511
  try:
340
- with console.status(f"[dim]Posting message {posted + 1}/{len(messages)}...[/dim]", spinner="dots"):
512
+ with console.status(f"[dim]({i}/{len(messages)}) {label}...[/dim]", spinner="dots"):
341
513
  codicent.post_message(content, type=msg_type)
514
+ console.print(f" [green]✓[/green] [dim]{label}[/dim]")
342
515
  posted += 1
343
516
  except Exception as e:
344
- console.print(f"[red]❌ Failed to post message: {e}[/red]")
517
+ console.print(f" [red]✗ {label}: {e}[/red]")
345
518
  failed += 1
346
519
 
347
520
  console.print()
@@ -350,6 +523,38 @@ def main():
350
523
  console.print(f"[red]❌ {failed} message(s) failed.[/red]")
351
524
  return 0 if failed == 0 else 1
352
525
 
526
+ # Handle wrap command
527
+ if len(sys.argv) > 1 and sys.argv[1] == "wrap":
528
+ from wrap import run_wrap_command
529
+
530
+ wrap_token = auth.get_cached_token()
531
+ if not wrap_token:
532
+ wrap_token = os.getenv("CODICENT_TOKEN")
533
+ if not wrap_token:
534
+ console.print("[red]❌ No authentication found.[/red]")
535
+ console.print("[dim]Run 'codicent auth' to authenticate, or set CODICENT_TOKEN environment variable[/dim]")
536
+ return 1
537
+
538
+ # Extract project and base_url from JWT token
539
+ try:
540
+ wrap_payload = wrap_token.split(".")[1]
541
+ wrap_payload += '=' * (-len(wrap_payload) % 4)
542
+ wrap_jwt = json.loads(base64.urlsafe_b64decode(wrap_payload).decode('utf-8'))
543
+ wrap_project = wrap_jwt["project"]
544
+ wrap_base_url = wrap_jwt.get("baseUrl", "https://codicent.com/")
545
+ except Exception as e:
546
+ console.print(f"[red]❌ Failed to extract project from token: {e}[/red]")
547
+ return 1
548
+
549
+ # Pass everything after "wrap" as args
550
+ wrap_args = sys.argv[2:]
551
+ return run_wrap_command(
552
+ args=wrap_args,
553
+ token=wrap_token,
554
+ base_url=wrap_base_url,
555
+ project=wrap_project,
556
+ )
557
+
353
558
  # Get authentication token
354
559
  token = auth.get_cached_token()
355
560
  if not token:
@@ -578,7 +783,7 @@ def main():
578
783
 
579
784
  header = Text()
580
785
  header.append("Codicent CLI", style="bold bright_cyan")
581
- header.append(" v0.7.1\n", style="bright_cyan")
786
+ header.append(f" v{CURRENT_VERSION}\n", style="bright_cyan")
582
787
  header.append("Type your questions or use Ctrl+C to exit.\n", style="dim")
583
788
  header.append("\n")
584
789
  header.append("Tip: ", style="bold dim")