cinna-cli 0.2.0__tar.gz → 0.2.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.
Files changed (43) hide show
  1. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/PKG-INFO +1 -1
  2. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/pyproject.toml +1 -1
  3. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/account.py +58 -4
  4. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/context.py +10 -2
  5. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/main.py +5 -4
  6. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/mcp_proxy.py +87 -11
  7. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_account.py +116 -8
  8. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_context.py +3 -0
  9. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/uv.lock +1 -1
  10. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/.github/workflows/publish.yml +0 -0
  11. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/.gitignore +0 -0
  12. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/LICENSE.md +0 -0
  13. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/README.md +0 -0
  14. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/docs/README.md +0 -0
  15. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/docs/interface.md +0 -0
  16. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/docs/mutagen_capabilities.md +0 -0
  17. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/__init__.py +0 -0
  18. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/auth.py +0 -0
  19. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/bootstrap.py +0 -0
  20. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/client.py +0 -0
  21. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/config.py +0 -0
  22. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/console.py +0 -0
  23. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/errors.py +0 -0
  24. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/logging.py +0 -0
  25. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/mutagen_runtime.py +0 -0
  26. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync.py +0 -0
  27. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync_session.py +0 -0
  28. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync_ssh_shim.py +0 -0
  29. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/sync_tui.py +0 -0
  30. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/templates/ACCOUNT_CLAUDE.md.template +0 -0
  31. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/templates/CLAUDE.md.template +0 -0
  32. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/src/cinna/templates/__init__.py +0 -0
  33. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/__init__.py +0 -0
  34. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/conftest.py +0 -0
  35. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_auth.py +0 -0
  36. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_bootstrap.py +0 -0
  37. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_client.py +0 -0
  38. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_config.py +0 -0
  39. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_main.py +0 -0
  40. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_mutagen_runtime.py +0 -0
  41. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_sync.py +0 -0
  42. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_sync_session.py +0 -0
  43. {cinna_cli-0.2.0 → cinna_cli-0.2.1}/tests/test_sync_ssh_shim.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinna-cli
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Local development CLI for Cinna Core agents
5
5
  Project-URL: Homepage, https://github.com/opencinna/cinna-cli
6
6
  Project-URL: Repository, https://github.com/opencinna/cinna-cli
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinna-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Local development CLI for Cinna Core agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -161,6 +161,44 @@ def parse_account_setup_input(raw_input: str) -> tuple[str, str]:
161
161
  return platform_url, text
162
162
 
163
163
 
164
+ def default_account_dir_name(platform_url: str) -> str:
165
+ """Derive a default workspace folder name from the platform domain.
166
+
167
+ e.g. ``https://demo-core.opencinna.io`` → ``demo-core_opencinna_io``.
168
+ Hostname only (creds/port stripped); every run of non
169
+ ``[A-Za-z0-9-]`` characters collapses to a single underscore. Falls back to
170
+ ``DEFAULT_ACCOUNT_DIR`` when the URL has no usable host.
171
+ """
172
+ host = urlparse(platform_url).netloc or platform_url.strip()
173
+ host = host.split("@")[-1].split(":")[0] # strip user:pass@ and :port
174
+ slug = re.sub(r"[^A-Za-z0-9-]+", "_", host).strip("_")
175
+ return slug or DEFAULT_ACCOUNT_DIR
176
+
177
+
178
+ def _prompt_account_dir(default: str) -> str:
179
+ """Ask for the workspace folder name, offering ``default``.
180
+
181
+ Works in the ``curl ... | python3 -`` bootstrap too: there stdin carries the
182
+ installer script (not keystrokes), so when stdin is not a TTY we talk to the
183
+ controlling terminal via ``/dev/tty`` as long as stdout is a TTY. With no
184
+ terminal attached (CI, captured output) we return ``default`` unchanged so
185
+ non-interactive runs stay non-interactive.
186
+ """
187
+ if sys.stdin.isatty():
188
+ return click.prompt("Workspace folder name", default=default)
189
+
190
+ if not sys.stdout.isatty():
191
+ return default
192
+ try:
193
+ with open("/dev/tty", "r+") as tty:
194
+ tty.write(f"Workspace folder name [{default}]: ")
195
+ tty.flush()
196
+ line = tty.readline()
197
+ except OSError:
198
+ return default
199
+ return line.strip() or default
200
+
201
+
164
202
  def _exchange_account_setup_token(
165
203
  platform_url: str, token: str, machine_name: str
166
204
  ) -> dict:
@@ -335,8 +373,13 @@ def _write_account_mcp_config(account_root: Path) -> None:
335
373
  ``POST /account/knowledge/search`` — the account analogue of the per-agent
336
374
  workspace's knowledge tool. Auto-generated infra: overwritten on every
337
375
  ``cinna account setup`` / ``cinna account refresh-context``.
376
+
377
+ The config path is written **relative** to the account root (anchored at the
378
+ launch cwd, which MCP clients set to the workspace folder) so the folder can
379
+ be moved without breaking the proxy. ``run_mcp_proxy`` additionally walks up
380
+ from cwd, which self-heals older configs that stored an absolute path.
338
381
  """
339
- account_config = str(account_config_path(account_root))
382
+ account_config = f"{CONFIG_DIR}/{ACCOUNT_CONFIG_FILE}"
340
383
 
341
384
  mcp_json = {
342
385
  "mcpServers": {
@@ -434,11 +477,23 @@ def _install_context_package(
434
477
 
435
478
 
436
479
  def run_account_setup(
437
- setup_input: str, machine_name: str, dir_name: str = DEFAULT_ACCOUNT_DIR
480
+ setup_input: str, machine_name: str, dir_name: str | None = None
438
481
  ) -> None:
439
- """Full account setup flow — called by `cinna account setup <token_or_url>`."""
482
+ """Full account setup flow — called by `cinna account setup <token_or_url>`.
483
+
484
+ When ``dir_name`` is not given (no ``--dir``), the folder name defaults to
485
+ the platform domain normalized (e.g. ``demo-core_opencinna_io``); the user
486
+ can accept it or type their own at the prompt.
487
+ """
440
488
  total = 3
441
489
 
490
+ # Parse before touching the filesystem / network so we can derive the
491
+ # default folder name from the platform domain (and fail fast on bad input).
492
+ platform_url, token = parse_account_setup_input(setup_input)
493
+
494
+ if not dir_name:
495
+ dir_name = _prompt_account_dir(default_account_dir_name(platform_url))
496
+
442
497
  # Guard the target directory before burning the single-use setup token.
443
498
  account_root = Path.cwd() / dir_name
444
499
  if account_config_path(account_root).exists():
@@ -449,7 +504,6 @@ def run_account_setup(
449
504
 
450
505
  # Step 1: Exchange the account setup token
451
506
  console.step(1, total, "Authenticating...")
452
- platform_url, token = parse_account_setup_input(setup_input)
453
507
  payload = _exchange_account_setup_token(platform_url, token, machine_name)
454
508
 
455
509
  # Step 2: Materialize the account workspace
@@ -114,6 +114,14 @@ def generate_context_files(
114
114
  (workspace_root / "CLAUDE.md").write_text(claude_md)
115
115
 
116
116
 
117
+ # Relative (workspace-root-anchored) path to the per-agent config. Written into
118
+ # the generated MCP configs so the workspace folder can be moved without
119
+ # breaking the proxy: MCP clients launch `cinna mcp-proxy` with cwd set to the
120
+ # workspace root, and the proxy resolves this path against cwd. (`run_mcp_proxy`
121
+ # also falls back to walking up from cwd, healing older absolute-path configs.)
122
+ _REL_CONFIG_PATH = ".cinna/config.json"
123
+
124
+
117
125
  def generate_mcp_json(config: CinnaConfig, workspace_root: Path) -> None:
118
126
  """Generate .mcp.json for Claude Code MCP tool discovery."""
119
127
  mcp_config = {
@@ -122,7 +130,7 @@ def generate_mcp_json(config: CinnaConfig, workspace_root: Path) -> None:
122
130
  "command": "cinna",
123
131
  "args": ["mcp-proxy"],
124
132
  "env": {
125
- "CINNA_CONFIG": str(workspace_root / ".cinna" / "config.json"),
133
+ "CINNA_CONFIG": _REL_CONFIG_PATH,
126
134
  },
127
135
  }
128
136
  }
@@ -138,7 +146,7 @@ def generate_opencode_json(config: CinnaConfig, workspace_root: Path) -> None:
138
146
  "type": "local",
139
147
  "command": ["cinna", "mcp-proxy"],
140
148
  "environment": {
141
- "CINNA_CONFIG": str(workspace_root / ".cinna" / "config.json"),
149
+ "CINNA_CONFIG": _REL_CONFIG_PATH,
142
150
  },
143
151
  "enabled": True,
144
152
  }
@@ -131,11 +131,12 @@ def account():
131
131
  @click.option(
132
132
  "--dir",
133
133
  "dir_name",
134
- default="my-cinna",
135
- show_default=True,
136
- help="Directory to create the account workspace in",
134
+ default=None,
135
+ help="Directory to create the account workspace in "
136
+ "(default: the platform domain, e.g. demo-core_opencinna_io; "
137
+ "you'll be prompted to accept or change it)",
137
138
  )
138
- def account_setup(setup_input: tuple[str, ...], name: str | None, dir_name: str):
139
+ def account_setup(setup_input: tuple[str, ...], name: str | None, dir_name: str | None):
139
140
  """Set up an account workspace from an account setup token.
140
141
 
141
142
  Accepts any of these formats (paste directly from Settings → Local
@@ -187,28 +187,105 @@ def _setup_mcp_logging(workspace_root: Path) -> None:
187
187
  root.addHandler(handler)
188
188
 
189
189
 
190
+ def _resolve_proxy_context() -> tuple[str | None, Path | None]:
191
+ """Resolve ``(mode, workspace_root)`` for the proxy, tolerant of a moved folder.
192
+
193
+ ``mode`` is ``"account"`` or ``"agent"``; ``workspace_root`` is the directory
194
+ holding ``.cinna/``. Returns ``(None, None)`` when nothing can be located.
195
+
196
+ The ``CINNA_ACCOUNT_CONFIG`` / ``CINNA_CONFIG`` env vars (set by the generated
197
+ ``.mcp.json`` / ``opencode.json``) primarily **select the mode**; their value
198
+ is only a hint for locating the config. We resolve the workspace root in order:
199
+
200
+ 1. The env value taken literally — an absolute path (legacy configs) or a
201
+ path relative to the launch cwd (the portable form newer configs write).
202
+ 2. A walk up from the launch cwd for the mode's config file. MCP clients
203
+ launch the proxy with cwd set to the workspace folder, so this heals
204
+ stale absolute paths after the folder is moved — no regeneration needed.
205
+
206
+ When neither env var is set, we auto-detect the nearest ``.cinna/`` from cwd
207
+ (an agent's ``config.json`` nested under an account wins over the account's
208
+ ``account.json`` because it is found first).
209
+ """
210
+ from cinna.account import ACCOUNT_CONFIG_FILE, find_account_root
211
+ from cinna.config import CONFIG_DIR, CONFIG_FILE, find_workspace_root
212
+ from cinna.errors import AccountConfigNotFoundError, ConfigNotFoundError
213
+
214
+ cwd = Path.cwd()
215
+ account_env = os.environ.get("CINNA_ACCOUNT_CONFIG")
216
+ config_env = os.environ.get("CINNA_CONFIG")
217
+
218
+ def _root_from_env(env_value: str | None, filename: str) -> Path | None:
219
+ """Workspace root if env_value points at an existing .cinna/<filename>."""
220
+ if not env_value:
221
+ return None
222
+ candidates = [Path(env_value)]
223
+ if not Path(env_value).is_absolute():
224
+ candidates.append(cwd / env_value)
225
+ for cand in candidates:
226
+ if cand.is_file():
227
+ return cand.resolve().parent.parent
228
+ return None
229
+
230
+ if account_env is not None:
231
+ root = _root_from_env(account_env, ACCOUNT_CONFIG_FILE)
232
+ if root is None:
233
+ try:
234
+ root = find_account_root(cwd)
235
+ except AccountConfigNotFoundError:
236
+ root = None
237
+ if root is not None:
238
+ return "account", root
239
+
240
+ if config_env is not None:
241
+ root = _root_from_env(config_env, CONFIG_FILE)
242
+ if root is None:
243
+ try:
244
+ root = find_workspace_root(cwd)
245
+ except ConfigNotFoundError:
246
+ root = None
247
+ if root is not None:
248
+ return "agent", root
249
+
250
+ # No usable env hint — auto-detect the nearest .cinna/ walking up from cwd.
251
+ if account_env is None and config_env is None:
252
+ current = cwd.resolve()
253
+ while True:
254
+ cinna_dir = current / CONFIG_DIR
255
+ if (cinna_dir / ACCOUNT_CONFIG_FILE).is_file():
256
+ return "account", current
257
+ if (cinna_dir / CONFIG_FILE).is_file():
258
+ return "agent", current
259
+ parent = current.parent
260
+ if parent == current:
261
+ break
262
+ current = parent
263
+
264
+ return None, None
265
+
266
+
190
267
  def run_mcp_proxy():
191
268
  """Entry point for `cinna mcp-proxy` — run as MCP stdio server.
192
269
 
193
- Two modes, selected by environment variable:
270
+ Two modes, selected by environment variable (see ``_resolve_proxy_context``
271
+ for how the workspace root is located in a move-tolerant way):
194
272
 
195
273
  * ``CINNA_ACCOUNT_CONFIG`` set → **account mode**: search the account user's
196
274
  platform knowledge sources (used by the account workspace's `.mcp.json`).
197
275
  * ``CINNA_CONFIG`` set → **per-agent mode**: search a single agent's
198
276
  knowledge base (used by a per-agent workspace's `.mcp.json`).
199
277
  """
200
- account_config_path = os.environ.get("CINNA_ACCOUNT_CONFIG")
201
- config_path = os.environ.get("CINNA_CONFIG")
202
- if not account_config_path and not config_path:
278
+ mode, workspace_root = _resolve_proxy_context()
279
+ if mode is None:
203
280
  raise SystemExit(
204
- "CINNA_CONFIG or CINNA_ACCOUNT_CONFIG environment variable not set"
281
+ "Could not locate a cinna workspace. Set CINNA_CONFIG or "
282
+ "CINNA_ACCOUNT_CONFIG, or run `cinna mcp-proxy` from inside a "
283
+ "workspace folder (one containing .cinna/)."
205
284
  )
206
285
 
207
- if account_config_path:
208
- # Account mode: <root>/.cinna/account.json → workspace root is parent.parent
209
- workspace_root = Path(account_config_path).parent.parent
286
+ if mode == "account":
210
287
  _setup_mcp_logging(workspace_root)
211
- logger.info("MCP proxy starting in account mode (config=%s)", account_config_path)
288
+ logger.info("MCP proxy starting in account mode (workspace=%s)", workspace_root)
212
289
 
213
290
  from cinna.account import load_account_config
214
291
 
@@ -224,9 +301,8 @@ def run_mcp_proxy():
224
301
  logger.exception("Failed to create account MCP server")
225
302
  raise
226
303
  else:
227
- workspace_root = Path(config_path).parent.parent
228
304
  _setup_mcp_logging(workspace_root)
229
- logger.info("MCP proxy starting (config=%s)", config_path)
305
+ logger.info("MCP proxy starting (workspace=%s)", workspace_root)
230
306
 
231
307
  try:
232
308
  config = load_config(workspace_root)
@@ -119,6 +119,25 @@ def test_parse_account_rejects_per_agent_url():
119
119
  parse_account_setup_input("https://h.example/api/cli-setup/tok_abc")
120
120
 
121
121
 
122
+ # --- default_account_dir_name ---
123
+
124
+
125
+ @pytest.mark.parametrize(
126
+ "platform_url,expected",
127
+ [
128
+ ("https://demo-core.opencinna.io", "demo-core_opencinna_io"),
129
+ ("https://demo-core.opencinna.io/api", "demo-core_opencinna_io"),
130
+ ("http://localhost:8000/api", "localhost"),
131
+ ("https://user:pass@app.example.com:443", "app_example_com"),
132
+ ("", "my-cinna"),
133
+ ],
134
+ )
135
+ def test_default_account_dir_name(platform_url, expected):
136
+ from cinna.account import default_account_dir_name
137
+
138
+ assert default_account_dir_name(platform_url) == expected
139
+
140
+
122
141
  # --- account config I/O ---
123
142
 
124
143
 
@@ -222,7 +241,8 @@ def test_account_setup_creates_workspace(
222
241
  )
223
242
  assert captured["body"]["machine_name"] == "laptop"
224
243
 
225
- root = tmp_path / "my-cinna"
244
+ # No --dir given → folder name defaults to the normalized platform domain.
245
+ root = tmp_path / "platform_example_com"
226
246
  cfg = load_account_config(root)
227
247
  assert cfg.account_token == "account-jwt-once"
228
248
  assert cfg.platform_url == "https://platform.example.com"
@@ -247,13 +267,11 @@ def test_account_setup_creates_workspace(
247
267
  proxy = mcp["mcpServers"]["platform-knowledge"]
248
268
  assert proxy["command"] == "cinna"
249
269
  assert proxy["args"] == ["mcp-proxy"]
250
- assert proxy["env"]["CINNA_ACCOUNT_CONFIG"] == str(account_config_path(root))
270
+ assert proxy["env"]["CINNA_ACCOUNT_CONFIG"] == ".cinna/account.json"
251
271
  opencode = json.loads((root / "opencode.json").read_text())
252
272
  oc_proxy = opencode["mcp"]["platform-knowledge"]
253
273
  assert oc_proxy["command"] == ["cinna", "mcp-proxy"]
254
- assert oc_proxy["environment"]["CINNA_ACCOUNT_CONFIG"] == str(
255
- account_config_path(root)
256
- )
274
+ assert oc_proxy["environment"]["CINNA_ACCOUNT_CONFIG"] == ".cinna/account.json"
257
275
 
258
276
  # Context package extracted under context/.
259
277
  mock_client.download_context_package.assert_called_once()
@@ -325,6 +343,8 @@ def test_account_setup_refuses_existing_workspace(
325
343
  "https://platform.example.com/api/cli-setup/account/TOK",
326
344
  "--name",
327
345
  "laptop",
346
+ "--dir",
347
+ "my-cinna",
328
348
  ],
329
349
  )
330
350
  assert result.exit_code != 0
@@ -376,14 +396,55 @@ def test_account_setup_continues_on_context_failure(
376
396
  assert "Context package download failed" in result.output
377
397
  assert "refresh-context" in result.output
378
398
 
379
- # The workspace itself is fully materialized.
380
- root = tmp_path / "my-cinna"
399
+ # The workspace itself is fully materialized (domain-derived default dir).
400
+ root = tmp_path / "platform_example_com"
381
401
  assert load_account_config(root).account_token == "account-jwt-once"
382
402
  assert (root / "agents").is_dir()
383
403
  assert (root / "CLAUDE.md").is_file()
384
404
  assert not (root / "context").exists()
385
405
 
386
406
 
407
+ @patch("cinna.account.AccountClient")
408
+ @patch("cinna.account.httpx.post")
409
+ def test_account_setup_explicit_dir_overrides_domain_default(
410
+ mock_post, mock_client_cls, runner, tmp_path, monkeypatch
411
+ ):
412
+ """An explicit ``--dir`` wins over the domain-derived default."""
413
+ monkeypatch.chdir(tmp_path)
414
+ mock_client = mock_client_cls.return_value.__enter__.return_value
415
+ mock_client.download_context_package.return_value = make_context_tarball()
416
+
417
+ class FakeResponse:
418
+ status_code = 200
419
+ text = ""
420
+
421
+ def json(self):
422
+ return {
423
+ "account_token": "account-jwt-once",
424
+ "platform_url": "https://platform.example.com",
425
+ "frontend_url": "https://ui.example.com",
426
+ "machine_name": "laptop",
427
+ }
428
+
429
+ mock_post.return_value = FakeResponse()
430
+
431
+ result = runner.invoke(
432
+ cli,
433
+ [
434
+ "account",
435
+ "setup",
436
+ "https://platform.example.com/api/cli-setup/account/TOK",
437
+ "--name",
438
+ "laptop",
439
+ "--dir",
440
+ "my-custom-folder",
441
+ ],
442
+ )
443
+ assert result.exit_code == 0, result.output
444
+ assert (tmp_path / "my-custom-folder" / ".cinna" / "account.json").is_file()
445
+ assert not (tmp_path / "platform_example_com").exists()
446
+
447
+
387
448
  # --- cinna account refresh-context ---
388
449
 
389
450
 
@@ -529,7 +590,8 @@ def test_write_account_mcp_config_wires_account_mode(account_root):
529
590
 
530
591
  _write_account_mcp_config(account_root)
531
592
 
532
- expected_cfg = str(account_config_path(account_root))
593
+ # Relative path (anchored at the workspace folder) so the folder is portable.
594
+ expected_cfg = ".cinna/account.json"
533
595
 
534
596
  mcp = json.loads((account_root / ".mcp.json").read_text())
535
597
  proxy = mcp["mcpServers"]["platform-knowledge"]
@@ -599,6 +661,52 @@ def account_cfg_token(account_root: Path) -> str:
599
661
  return load_account_config(account_root).account_token
600
662
 
601
663
 
664
+ # --- move-tolerant proxy context resolution ---
665
+
666
+
667
+ def test_proxy_context_relative_path_resolved_from_cwd(account_root, monkeypatch):
668
+ """A relative CINNA_ACCOUNT_CONFIG resolves against the launch cwd (the
669
+ workspace folder MCP clients cd into) — the portable form newer configs write."""
670
+ from cinna.mcp_proxy import _resolve_proxy_context
671
+
672
+ monkeypatch.chdir(account_root)
673
+ monkeypatch.setenv("CINNA_ACCOUNT_CONFIG", ".cinna/account.json")
674
+ monkeypatch.delenv("CINNA_CONFIG", raising=False)
675
+
676
+ mode, root = _resolve_proxy_context()
677
+ assert mode == "account"
678
+ assert root == account_root.resolve()
679
+
680
+
681
+ def test_proxy_context_stale_absolute_path_heals_via_cwd(account_root, monkeypatch):
682
+ """A stale absolute CINNA_ACCOUNT_CONFIG (folder moved) falls back to walking
683
+ up from cwd, so an unregenerated legacy config keeps working after a move."""
684
+ from cinna.mcp_proxy import _resolve_proxy_context
685
+
686
+ monkeypatch.chdir(account_root)
687
+ monkeypatch.setenv(
688
+ "CINNA_ACCOUNT_CONFIG", "/old/location/my-cinna/.cinna/account.json"
689
+ )
690
+ monkeypatch.delenv("CINNA_CONFIG", raising=False)
691
+
692
+ mode, root = _resolve_proxy_context()
693
+ assert mode == "account"
694
+ assert root == account_root.resolve()
695
+
696
+
697
+ def test_proxy_context_autodetects_nearest_cinna_without_env(account_root, monkeypatch):
698
+ """With no env hint, the proxy auto-detects the nearest .cinna/ from cwd."""
699
+ from cinna.mcp_proxy import _resolve_proxy_context
700
+
701
+ monkeypatch.chdir(account_root)
702
+ monkeypatch.delenv("CINNA_ACCOUNT_CONFIG", raising=False)
703
+ monkeypatch.delenv("CINNA_CONFIG", raising=False)
704
+
705
+ mode, root = _resolve_proxy_context()
706
+ assert mode == "account"
707
+ assert root == account_root.resolve()
708
+
709
+
602
710
  # --- cinna account agents ---
603
711
 
604
712
 
@@ -43,6 +43,8 @@ def test_generate_mcp_json(workspace_root, sample_config):
43
43
  server = data["mcpServers"]["agent-knowledge"]
44
44
  assert server["command"] == "cinna"
45
45
  assert server["args"] == ["mcp-proxy"]
46
+ # Relative path so the workspace folder stays portable when moved.
47
+ assert server["env"]["CINNA_CONFIG"] == ".cinna/config.json"
46
48
 
47
49
 
48
50
  def test_generate_opencode_json(workspace_root, sample_config):
@@ -60,6 +62,7 @@ def test_generate_opencode_json(workspace_root, sample_config):
60
62
  assert server["type"] == "local"
61
63
  assert server["command"] == ["cinna", "mcp-proxy"]
62
64
  assert server["enabled"] is True
65
+ assert server["environment"]["CINNA_CONFIG"] == ".cinna/config.json"
63
66
 
64
67
 
65
68
  def test_generate_gitignore(workspace_root):
@@ -136,7 +136,7 @@ wheels = [
136
136
 
137
137
  [[package]]
138
138
  name = "cinna-cli"
139
- version = "0.2.0"
139
+ version = "0.2.1"
140
140
  source = { editable = "." }
141
141
  dependencies = [
142
142
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes