deepsieve-cli 0.1.0__tar.gz → 0.1.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.
@@ -17,7 +17,20 @@
17
17
  .aws/
18
18
 
19
19
  # --- Node / Next.js (site) ---
20
- site/node_modules/
20
+ # Unanchored on purpose: `site/node_modules/` alone left a ROOT-level
21
+ # node_modules/ tracked, and running vitest from the repo root (rather than from
22
+ # site/) creates one — `node_modules/.vite/vitest/**/results.json` reached
23
+ # `development` in #320 through a `git add -A`, passing review and CI without
24
+ # anything failing. A rule that covers the path someone already hit is not a rule.
25
+ # ⚠️ NO TRAILING SLASH, and that is load-bearing too. `node_modules/` matches
26
+ # DIRECTORIES ONLY, so a SYMLINKED node_modules is not ignored and `git add -A`
27
+ # stages the link itself — an absolute path that resolves for nobody else (gh
28
+ # #756). Symlinking it is the documented way to share one 733 MB install across
29
+ # git worktrees on a full disk, so the ignore rule and the disk guidance
30
+ # disagreed silently. Verified by controlled test: with `node_modules/`,
31
+ # `git add -A` reports `A node_modules`; without the slash it reports nothing.
32
+ node_modules
33
+ site/node_modules
21
34
  site/.next/
22
35
  site/.next-*/
23
36
  site/out/
@@ -113,4 +126,9 @@ api/_stress/
113
126
  # Stray Playwright accessibility-snapshot dumps from browser-driven QA.
114
127
  ctx*-run.md
115
128
  page-*.yml
116
- user-testing/
129
+
130
+ # Antigravity worktrees and state
131
+ .gemini/
132
+
133
+ # Antigravity (Gemini) agent local config — per-machine, like .claude/settings.local.json
134
+ .agents/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: deepsieve-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: DeepSieve CLI — run cited deep research from your terminal
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27
@@ -29,6 +29,7 @@ deepsieve runs create --query "..." --wait # real run: spends credits
29
29
  deepsieve runs list
30
30
  deepsieve runs get <id> # exit 4 while still running
31
31
  deepsieve runs cancel <id>
32
+ deepsieve members list # who is in the workspace + seat usage
32
33
  deepsieve data catalog # entities + columns (never guess)
33
34
  deepsieve data get companies --receipts # rows with per-cell citations
34
35
  deepsieve setup mcp # MCP registration for this origin
@@ -21,6 +21,7 @@ deepsieve runs create --query "..." --wait # real run: spends credits
21
21
  deepsieve runs list
22
22
  deepsieve runs get <id> # exit 4 while still running
23
23
  deepsieve runs cancel <id>
24
+ deepsieve members list # who is in the workspace + seat usage
24
25
  deepsieve data catalog # entities + columns (never guess)
25
26
  deepsieve data get companies --receipts # rows with per-cell citations
26
27
  deepsieve setup mcp # MCP registration for this origin
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepsieve-cli"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "DeepSieve CLI — run cited deep research from your terminal"
5
5
  requires-python = ">=3.11"
6
6
  readme = "README.md"
@@ -238,8 +238,11 @@ def cmd_runs_get(args) -> int:
238
238
 
239
239
  def cmd_runs_create(args) -> int:
240
240
  client, prof = _require_auth(args)
241
- if bool(args.query) == bool(args.seeds):
242
- err("pass exactly one of --query or --seeds")
241
+ # At most one. NEITHER is a paid broad sweep of the whole Blueprint schema
242
+ # (#1005), which the API accepts; the CLI used to refuse it as a usage
243
+ # error and could not express the feature at all.
244
+ if args.query and args.seeds:
245
+ err("pass at most one of --query or --seeds")
243
246
  return EXIT_USAGE
244
247
 
245
248
  body: dict[str, Any] = {"depth": args.depth, "dry_run": args.dry_run, "monitored": args.monitored}
@@ -322,6 +325,27 @@ def cmd_runs_cancel(args) -> int:
322
325
  return EXIT_OK
323
326
 
324
327
 
328
+ def cmd_members_list(args) -> int:
329
+ """Who is in this workspace and how many editor seats are in use (#985).
330
+ Read-only; inviting is a human-admin action in the UI."""
331
+ client, _ = _require_auth(args)
332
+ try:
333
+ body = client.request("GET", "/v1/members")
334
+ except ApiError as exc:
335
+ err(str(exc))
336
+ return EXIT_FAIL
337
+ if args.json:
338
+ out(body, True)
339
+ return EXIT_OK
340
+ for m in body.get("data", []):
341
+ print(f"{m.get('email')} {m.get('role')} ({m.get('status')})")
342
+ seats = body.get("seats") or {}
343
+ inc = seats.get("editor_seats_included")
344
+ used = seats.get("editor_seats_used")
345
+ print(f"\neditor seats: {used} used / {'unlimited' if inc is None else inc} included")
346
+ return EXIT_OK
347
+
348
+
325
349
  def cmd_data_catalog(args) -> int:
326
350
  client, _ = _require_auth(args)
327
351
  try:
@@ -484,6 +508,10 @@ def build_parser() -> argparse.ArgumentParser:
484
508
  rx.add_argument("run_id")
485
509
  rx.set_defaults(func=cmd_runs_cancel)
486
510
 
511
+ members = sub.add_parser("members", help="your workspace's team", parents=[common]).add_subparsers(dest="sub", required=True)
512
+ ml = members.add_parser("list", help="who is in the workspace, and seat usage", parents=[common])
513
+ ml.set_defaults(func=cmd_members_list)
514
+
487
515
  data = sub.add_parser("data", help="your cited dataset", parents=[common]).add_subparsers(dest="sub", required=True)
488
516
 
489
517
  dc = data.add_parser("catalog", help="entities and columns (never guess these)", parents=[common])
@@ -207,10 +207,27 @@ def test_dry_run_needs_no_confirmation(monkeypatch):
207
207
  assert seen["idem"], "runs cost money — every create must carry an idempotency key"
208
208
 
209
209
 
210
- def test_run_create_requires_exactly_one_of_query_or_seeds(monkeypatch, capsys):
210
+ def test_run_create_refuses_both_query_and_seeds(monkeypatch, capsys):
211
211
  cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="k"))
212
- assert main(["runs", "create"]) == 2
213
- assert "exactly one" in capsys.readouterr().err
212
+ assert main(["runs", "create", "--query", "x", "--seeds", "a,b"]) == 2
213
+ assert "at most one" in capsys.readouterr().err
214
+
215
+
216
+ def test_run_create_with_neither_sends_an_empty_broad_sweep(monkeypatch):
217
+ """#1005: neither flag is a schema-derived broad sweep, not a usage error.
218
+ The body must carry NEITHER key — the API treats their absence as the
219
+ sweep, and a `query: ""` would be a validation error instead."""
220
+ cfg.save_profile(cfg.Profile(name="default", origin="https://x.test", api_key="k"))
221
+ seen = {}
222
+
223
+ def handler(request: httpx.Request) -> httpx.Response:
224
+ seen["body"] = json.loads(request.content)
225
+ return httpx.Response(202, json={"id": "run_1", "status": "queued"})
226
+
227
+ monkeypatch.setattr(httpx, "Client", _mock(handler))
228
+ assert main(["runs", "create", "--dry-run"]) == 0
229
+ assert "query" not in seen["body"] and "seeds" not in seen["body"]
230
+ assert seen["body"]["dry_run"] is True
214
231
 
215
232
 
216
233
  def test_truncated_dataset_is_called_out(monkeypatch, capsys):
@@ -354,3 +371,76 @@ def test_failed_run_exits_failure_not_success(monkeypatch):
354
371
 
355
372
  monkeypatch.setattr(httpx, "Client", _mock(handler))
356
373
  assert main(["runs", "get", "r"]) == 1
374
+
375
+
376
+ # ── members list (#985) ──────────────────────────────────────────────────────
377
+
378
+
379
+ def _members_body(*, included=10, viewers=True):
380
+ return {
381
+ "object": "list",
382
+ "data": [
383
+ {"object": "member", "id": "m1", "user_id": "u1", "email": "owner@example.com",
384
+ "role": "owner", "status": "active", "workspace_ids": [], "created_at": None},
385
+ {"object": "member", "id": "m2", "user_id": "u2", "email": "reader@example.com",
386
+ "role": "viewer", "status": "active", "workspace_ids": ["ws1"], "created_at": None},
387
+ ],
388
+ "has_more": False,
389
+ "next_cursor": None,
390
+ "seats": {"editor_seats_used": 1, "editor_seats_included": included,
391
+ "editor_seats_free_now": None if included is None else included - 1,
392
+ "viewer_seats_available": viewers},
393
+ }
394
+
395
+
396
+ def _signed_in(monkeypatch):
397
+ monkeypatch.setenv(cfg.ENV_KEY, "ds_live_test")
398
+ monkeypatch.setenv(cfg.ENV_ORIGIN, "https://ci.test")
399
+
400
+
401
+ def test_members_list_prints_the_roster_and_seat_usage(monkeypatch, capsys):
402
+ """Review of #1022 found `members list` shipped with no test at all. Drives
403
+ the real command through the real client against a mocked /v1/members."""
404
+ _signed_in(monkeypatch)
405
+ seen = {}
406
+
407
+ def handler(request: httpx.Request) -> httpx.Response:
408
+ seen["path"] = request.url.path
409
+ seen["auth"] = request.headers.get("authorization")
410
+ return httpx.Response(200, json=_members_body())
411
+
412
+ monkeypatch.setattr(httpx, "Client", _mock(handler))
413
+ assert main(["members", "list"]) == 0
414
+ out_ = capsys.readouterr().out
415
+ assert seen["path"] == "/v1/members"
416
+ assert seen["auth"] == "Bearer ds_live_test"
417
+ assert "owner@example.com owner (active)" in out_
418
+ assert "reader@example.com viewer (active)" in out_
419
+ assert "editor seats: 1 used / 10 included" in out_
420
+
421
+
422
+ def test_members_list_json_is_the_raw_body_and_unlimited_reads_as_such(monkeypatch, capsys):
423
+ _signed_in(monkeypatch)
424
+ body = _members_body(included=None) # Enterprise: null = unlimited
425
+ monkeypatch.setattr(httpx, "Client", _mock(lambda r: httpx.Response(200, json=body)))
426
+ assert main(["members", "list", "--json"]) == 0
427
+ assert json.loads(capsys.readouterr().out) == body
428
+ # and the human form renders null as "unlimited", never "None". Client is
429
+ # still patched from above — patching again would wrap the patched factory
430
+ # and pass `transport` twice (that exact TypeError shipped in a red commit).
431
+ assert main(["members", "list"]) == 0
432
+ text = capsys.readouterr().out
433
+ assert "unlimited included" in text and "None" not in text
434
+
435
+
436
+ def test_members_list_surfaces_a_scope_refusal_as_a_typed_error(monkeypatch, capsys):
437
+ """An `agent`-preset key lacks members:read; the CLI must show the API's
438
+ code + message and exit non-zero, not stack-trace or print an empty roster."""
439
+ _signed_in(monkeypatch)
440
+ err_body = {"error": {"type": "permission_error", "code": "insufficient_scope",
441
+ "message": "this key lacks members:read", "retriable": False}}
442
+ monkeypatch.setattr(httpx, "Client", _mock(lambda r: httpx.Response(403, json=err_body)))
443
+ assert main(["members", "list"]) == 1
444
+ captured = capsys.readouterr()
445
+ assert "insufficient_scope" in captured.err or "members:read" in captured.err
446
+ assert "owner@example.com" not in captured.out