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.
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/.gitignore +20 -2
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/PKG-INFO +3 -2
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/README.md +1 -0
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/pyproject.toml +1 -1
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/src/deepsieve_cli/main.py +30 -2
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/tests/test_cli.py +93 -3
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/src/deepsieve_cli/__init__.py +0 -0
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/src/deepsieve_cli/__main__.py +0 -0
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/src/deepsieve_cli/client.py +0 -0
- {deepsieve_cli-0.1.0 → deepsieve_cli-0.1.1}/src/deepsieve_cli/config.py +0 -0
|
@@ -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
|
-
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: deepsieve-cli
|
|
3
|
-
Version: 0.1.
|
|
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
|
|
@@ -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
|
-
|
|
242
|
-
|
|
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
|
|
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 "
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|