abstractcode 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

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.
abstractcode/cli.py CHANGED
@@ -4,9 +4,10 @@ import argparse
4
4
  import os
5
5
  import sys
6
6
  from pathlib import Path
7
- from typing import Optional, Sequence
7
+ from typing import Optional, Sequence, TYPE_CHECKING
8
8
 
9
- from .react_shell import ReactShell
9
+ if TYPE_CHECKING: # pragma: no cover
10
+ from .react_shell import ReactShell
10
11
 
11
12
 
12
13
  def _default_state_file() -> str:
@@ -42,6 +43,70 @@ def _default_max_tokens() -> Optional[int]:
42
43
  return -1 # Auto (use model capabilities)
43
44
 
44
45
 
46
+ def _env_flag(name: str, *, default: bool = False) -> bool:
47
+ raw = os.getenv(name)
48
+ if raw is None:
49
+ return bool(default)
50
+ val = str(raw).strip().lower()
51
+ if val in {"1", "true", "yes", "y", "on"}:
52
+ return True
53
+ if val in {"0", "false", "no", "n", "off"}:
54
+ return False
55
+ return bool(default)
56
+
57
+
58
+ def _configure_abstractcode_logging(argv_list: Sequence[str]) -> None:
59
+ """Silence Python/Structured logs for the interactive TUI (default).
60
+
61
+ Rationale: log lines from providers (httpx, AbstractCore providers, etc) break the
62
+ terminal UI and are noisy. AbstractCode should surface errors in its own UI instead.
63
+
64
+ Override:
65
+ - Set `ABSTRACTCODE_SILENCE_LOGS=0` to keep standard logging.
66
+ """
67
+ if any(arg in {"-h", "--help"} for arg in argv_list):
68
+ # Help should be fast and quiet.
69
+ try:
70
+ import logging
71
+
72
+ logging.disable(logging.CRITICAL)
73
+ except Exception:
74
+ pass
75
+ return
76
+
77
+ if not _env_flag("ABSTRACTCODE_SILENCE_LOGS", default=True):
78
+ return
79
+
80
+ try:
81
+ import logging
82
+ from abstractcore.utils import structured_logging
83
+
84
+ defaults = structured_logging._get_config_defaults()
85
+ log_dir = defaults.get("log_dir")
86
+ file_level = defaults.get("file_level") if log_dir else None
87
+
88
+ structured_logging.configure_logging(
89
+ console_level=None,
90
+ file_level=file_level,
91
+ log_dir=log_dir,
92
+ verbatim_enabled=bool(defaults.get("verbatim_enabled", True)),
93
+ console_json=bool(defaults.get("console_json", False)),
94
+ file_json=bool(defaults.get("file_json", True)),
95
+ )
96
+
97
+ # Defensive: if libraries attach their own handlers, keep them quiet.
98
+ for name in ("httpx", "httpcore", "asyncio", "urllib3"):
99
+ logging.getLogger(name).setLevel(logging.WARNING)
100
+ except Exception:
101
+ # Best-effort: suppress any remaining logging noise.
102
+ try:
103
+ import logging
104
+
105
+ logging.disable(logging.CRITICAL)
106
+ except Exception:
107
+ pass
108
+
109
+
45
110
  def build_agent_parser() -> argparse.ArgumentParser:
46
111
  parser = argparse.ArgumentParser(
47
112
  prog="abstractcode",
@@ -49,6 +114,7 @@ def build_agent_parser() -> argparse.ArgumentParser:
49
114
  epilog=(
50
115
  "Workflows:\n"
51
116
  " abstractcode flow --help Run AbstractFlow workflows from the terminal\n"
117
+ " abstractcode workflow --help Install/list workflow bundles\n"
52
118
  "REPL:\n"
53
119
  " Use /flow inside the REPL to run workflows while keeping chat context.\n"
54
120
  ),
@@ -60,7 +126,10 @@ def build_agent_parser() -> argparse.ArgumentParser:
60
126
  help=(
61
127
  "Agent selector:\n"
62
128
  " - Built-ins: react | codeact | memact\n"
63
- " - Workflow agent: <flow_id> | <flow_name> | </path/to/flow.json>\n"
129
+ " - Workflow agent:\n"
130
+ " <flow_id> | <flow_name> | </path/to/flow.json>\n"
131
+ " <bundle_id>[@version] | </path/to/bundle.flow>\n"
132
+ " <bundle_id>[@version]:<flow_id>\n"
64
133
  " (must implement interface 'abstractcode.agent.v1')"
65
134
  ),
66
135
  )
@@ -124,10 +193,385 @@ def build_agent_parser() -> argparse.ArgumentParser:
124
193
  default=_default_max_tokens(),
125
194
  help="Maximum context tokens for LLM calls (-1 = auto from model capabilities).",
126
195
  )
196
+ parser.add_argument(
197
+ "--prompt",
198
+ default=None,
199
+ help="Run a single prompt and exit (supports @file mentions).",
200
+ )
127
201
  parser.add_argument("--no-color", action="store_true", help="Disable ANSI colors")
202
+ parser.add_argument(
203
+ "--gateway-url",
204
+ default=None,
205
+ help=(
206
+ "AbstractGateway base URL (for host metrics like /gpu).\n"
207
+ "Overrides $ABSTRACTCODE_GATEWAY_URL for this run."
208
+ ),
209
+ )
210
+ parser.add_argument(
211
+ "--gateway-token",
212
+ default=None,
213
+ help=(
214
+ "AbstractGateway auth token (Bearer) (for host metrics like /gpu).\n"
215
+ "Overrides $ABSTRACTCODE_GATEWAY_TOKEN for this run (not persisted)."
216
+ ),
217
+ )
218
+ return parser
219
+
220
+
221
+ def build_workflow_parser() -> argparse.ArgumentParser:
222
+ parser = argparse.ArgumentParser(
223
+ prog="abstractcode workflow",
224
+ description="Manage WorkflowBundle (.flow) bundles on an AbstractGateway host (upload/remove/discovery).",
225
+ )
226
+ sub = parser.add_subparsers(dest="command")
227
+
228
+ common = argparse.ArgumentParser(add_help=False)
229
+ common.add_argument("--gateway-url", default=None, help="Gateway base URL (default: $ABSTRACTCODE_GATEWAY_URL)")
230
+ common.add_argument("--gateway-token", default=None, help="Gateway auth token (default: $ABSTRACTCODE_GATEWAY_TOKEN)")
231
+
232
+ install = sub.add_parser("install", parents=[common], help="Upload/install a .flow bundle onto the gateway")
233
+ install.add_argument("source", help="Path to a .flow file")
234
+ install.add_argument("--overwrite", action="store_true", help="Overwrite if already installed")
235
+ install.add_argument("--json", action="store_true", help="Output JSON")
236
+
237
+ ls = sub.add_parser("list", parents=[common], help="List available workflow entrypoints (from gateway bundles)")
238
+ ls.add_argument("--interface", default=None, help="Filter entrypoints by interface id")
239
+ ls.add_argument("--all", action="store_true", help="Include all versions (default: latest only)")
240
+ ls.add_argument("--include-deprecated", action="store_true", help="Include deprecated workflows")
241
+ ls.add_argument("--json", action="store_true", help="Output JSON")
242
+
243
+ info = sub.add_parser("info", parents=[common], help="Show details for an installed bundle")
244
+ info.add_argument("bundle", help="Bundle ref: bundle_id or bundle_id@version")
245
+ info.add_argument("--json", action="store_true", help="Output JSON")
246
+
247
+ rm = sub.add_parser("remove", parents=[common], help="Remove an installed bundle (bundle_id or bundle_id@version)")
248
+ rm.add_argument("bundle", help="Bundle ref: bundle_id or bundle_id@version")
249
+ rm.add_argument("--json", action="store_true", help="Output JSON")
250
+
251
+ dep = sub.add_parser("deprecate", parents=[common], help="Deprecate a workflow bundle on the gateway (hide + block launch)")
252
+ dep.add_argument("bundle", help="Bundle id (bundle_id)")
253
+ dep.add_argument("--flow-id", default=None, help="Optional entrypoint flow_id (default: all entrypoints)")
254
+ dep.add_argument("--reason", default=None, help="Optional reason")
255
+ dep.add_argument("--json", action="store_true", help="Output JSON")
256
+
257
+ undep = sub.add_parser("undeprecate", parents=[common], help="Undeprecate a workflow bundle on the gateway")
258
+ undep.add_argument("bundle", help="Bundle id (bundle_id)")
259
+ undep.add_argument("--flow-id", default=None, help="Optional entrypoint flow_id (default: all entrypoints)")
260
+ undep.add_argument("--json", action="store_true", help="Output JSON")
261
+
128
262
  return parser
129
263
 
130
264
 
265
+ def _run_one_shot_prompt(*, shell: ReactShell, prompt: str) -> int:
266
+ """Run one task and exit (no full-screen UI)."""
267
+ from .file_mentions import extract_at_file_mentions, normalize_relative_path
268
+ from .flow_cli import _ApprovalState, _approve_and_execute
269
+
270
+ # Lazy imports: keep `abstractcode --help` fast.
271
+ from abstractruntime.core.models import RunStatus, WaitReason
272
+
273
+ text = str(prompt or "").strip()
274
+ if not text:
275
+ return 0
276
+
277
+ def _stderr_print(msg: str) -> None:
278
+ print(msg, file=sys.stderr)
279
+
280
+ cleaned, mentions = extract_at_file_mentions(text)
281
+ paths: list[str] = []
282
+ for m in mentions:
283
+ norm = normalize_relative_path(m)
284
+ if norm:
285
+ paths.append(norm)
286
+
287
+ # De-dup while preserving order.
288
+ seen: set[str] = set()
289
+ paths = [p for p in paths if not (p in seen or seen.add(p))]
290
+
291
+ attachment_refs = shell._ingest_workspace_attachments(paths) if paths else []
292
+ if attachment_refs:
293
+ joined = ", ".join(
294
+ [
295
+ str(a.get("source_path") or a.get("filename") or "?")
296
+ for a in attachment_refs
297
+ if isinstance(a, dict)
298
+ ]
299
+ )
300
+ if joined:
301
+ print(f"Attachments: {joined}", file=sys.stderr)
302
+
303
+ cleaned = str(cleaned or "").strip()
304
+ if not cleaned:
305
+ # Attachment-only invocation: allow users to attach files without issuing a prompt.
306
+ return 0
307
+
308
+ run_id = shell._agent.start(cleaned, allowed_tools=shell._allowed_tools, attachments=attachment_refs or None)
309
+ try:
310
+ shell._sync_tool_prompt_settings_to_run(run_id)
311
+ except Exception:
312
+ pass
313
+ if getattr(shell, "_state_file", None):
314
+ try:
315
+ shell._agent.save_state(shell._state_file) # type: ignore[arg-type]
316
+ except Exception:
317
+ pass
318
+
319
+ approval_state = _ApprovalState()
320
+
321
+ def _drive_subworkflow_wait(*, top_run_id: str) -> int:
322
+ """Drive async subworkflow waits until top run can advance or blocks on a real wait."""
323
+
324
+ def _extract_sub_run_id(wait_state: object) -> Optional[str]:
325
+ details = getattr(wait_state, "details", None)
326
+ if isinstance(details, dict):
327
+ sub_run_id = details.get("sub_run_id")
328
+ if isinstance(sub_run_id, str) and sub_run_id:
329
+ return sub_run_id
330
+ wait_key = getattr(wait_state, "wait_key", None)
331
+ if isinstance(wait_key, str) and wait_key.startswith("subworkflow:"):
332
+ return wait_key.split("subworkflow:", 1)[1] or None
333
+ return None
334
+
335
+ def _workflow_for(run_state: object):
336
+ reg = getattr(shell._runtime, "workflow_registry", None)
337
+ getter = getattr(reg, "get", None) if reg is not None else None
338
+ if callable(getter):
339
+ wf = getter(run_state.workflow_id)
340
+ if wf is not None:
341
+ return wf
342
+ if getattr(shell._agent.workflow, "workflow_id", None) == run_state.workflow_id:
343
+ return shell._agent.workflow
344
+ raise RuntimeError(f"Workflow '{run_state.workflow_id}' not found in runtime registry")
345
+
346
+ def _bubble_completion(child_state: object) -> Optional[str]:
347
+ parent_id = getattr(child_state, "parent_run_id", None)
348
+ if not isinstance(parent_id, str) or not parent_id:
349
+ return None
350
+ parent_state = shell._runtime.get_state(parent_id)
351
+ parent_wait = getattr(parent_state, "waiting", None)
352
+ if parent_state.status != RunStatus.WAITING or parent_wait is None:
353
+ return None
354
+ if parent_wait.reason != WaitReason.SUBWORKFLOW:
355
+ return None
356
+ shell._runtime.resume(
357
+ workflow=_workflow_for(parent_state),
358
+ run_id=parent_id,
359
+ wait_key=None,
360
+ payload={
361
+ "sub_run_id": child_state.run_id,
362
+ "output": getattr(child_state, "output", None),
363
+ "node_traces": shell._runtime.get_node_traces(child_state.run_id),
364
+ },
365
+ max_steps=0,
366
+ )
367
+ return parent_id
368
+
369
+ # Drive subruns until we either make progress or hit a non-subworkflow wait.
370
+ for _ in range(200):
371
+ # Descend to the deepest sub-run referenced by SUBWORKFLOW waits.
372
+ current_run_id = top_run_id
373
+ for _ in range(25):
374
+ cur_state = shell._runtime.get_state(current_run_id)
375
+ cur_wait = getattr(cur_state, "waiting", None)
376
+ if cur_state.status != RunStatus.WAITING or cur_wait is None:
377
+ break
378
+ if cur_wait.reason != WaitReason.SUBWORKFLOW:
379
+ break
380
+ next_id = _extract_sub_run_id(cur_wait)
381
+ if not next_id:
382
+ break
383
+ current_run_id = next_id
384
+
385
+ current_state = shell._runtime.get_state(current_run_id)
386
+
387
+ # Tick running subruns until they block/complete.
388
+ if current_state.status == RunStatus.RUNNING:
389
+ current_state = shell._runtime.tick(
390
+ workflow=_workflow_for(current_state),
391
+ run_id=current_run_id,
392
+ max_steps=100,
393
+ )
394
+
395
+ if current_state.status == RunStatus.RUNNING:
396
+ continue
397
+
398
+ if current_state.status == RunStatus.FAILED:
399
+ _stderr_print(f"Run failed: {current_state.error or 'Subworkflow failed'}")
400
+ return 1
401
+
402
+ if current_state.status == RunStatus.CANCELLED:
403
+ _stderr_print("Run cancelled.")
404
+ return 1
405
+
406
+ if current_state.status == RunStatus.WAITING:
407
+ cur_wait = getattr(current_state, "waiting", None)
408
+ if cur_wait is None:
409
+ break
410
+ if cur_wait.reason == WaitReason.SUBWORKFLOW:
411
+ continue
412
+
413
+ if cur_wait.reason == WaitReason.USER:
414
+ prompt_text = str(cur_wait.prompt or "Please respond:").strip()
415
+ response = input(prompt_text + " ")
416
+ shell._runtime.resume(
417
+ workflow=_workflow_for(current_state),
418
+ run_id=current_run_id,
419
+ wait_key=cur_wait.wait_key,
420
+ payload={"response": response},
421
+ )
422
+ continue
423
+
424
+ if cur_wait.reason == WaitReason.EVENT:
425
+ details = cur_wait.details if isinstance(cur_wait.details, dict) else {}
426
+ tool_calls = details.get("tool_calls")
427
+ if isinstance(tool_calls, list):
428
+ payload = _approve_and_execute(
429
+ tool_calls=tool_calls,
430
+ tool_runner=shell._tool_runner,
431
+ auto_approve=bool(shell._auto_approve),
432
+ approval_state=approval_state,
433
+ prompt_fn=input,
434
+ print_fn=_stderr_print,
435
+ )
436
+ if payload is None:
437
+ _stderr_print("Aborted (tool calls not executed).")
438
+ return 1
439
+ shell._runtime.resume(
440
+ workflow=_workflow_for(current_state),
441
+ run_id=current_run_id,
442
+ wait_key=cur_wait.wait_key,
443
+ payload=payload,
444
+ )
445
+ continue
446
+
447
+ if isinstance(cur_wait.prompt, str) and cur_wait.prompt.strip() and isinstance(cur_wait.wait_key, str) and cur_wait.wait_key:
448
+ response = input(cur_wait.prompt.strip() + " ")
449
+ shell._runtime.resume(
450
+ workflow=_workflow_for(current_state),
451
+ run_id=current_run_id,
452
+ wait_key=cur_wait.wait_key,
453
+ payload={"response": response},
454
+ )
455
+ continue
456
+
457
+ _stderr_print(f"Run waiting: {cur_wait.reason.value} ({cur_wait.wait_key})")
458
+ return 2
459
+
460
+ if current_state.status != RunStatus.COMPLETED:
461
+ break
462
+
463
+ parent_id = _bubble_completion(current_state)
464
+ if not parent_id:
465
+ break
466
+ if parent_id == top_run_id:
467
+ break
468
+
469
+ return 0
470
+
471
+ state = None
472
+ while True:
473
+ state = shell._agent.step()
474
+ if state.status in (RunStatus.COMPLETED, RunStatus.FAILED, RunStatus.CANCELLED):
475
+ break
476
+
477
+ if state.status != RunStatus.WAITING or not getattr(state, "waiting", None):
478
+ continue
479
+
480
+ wait = state.waiting
481
+
482
+ if wait.reason == WaitReason.USER:
483
+ prompt_text = str(wait.prompt or "Please respond:").strip()
484
+ response = input(prompt_text + " ")
485
+ shell._agent.resume(response)
486
+ continue
487
+
488
+ if wait.reason == WaitReason.SUBWORKFLOW:
489
+ rc = _drive_subworkflow_wait(top_run_id=run_id)
490
+ if rc != 0:
491
+ return rc
492
+ continue
493
+
494
+ if wait.reason == WaitReason.EVENT:
495
+ details = wait.details or {}
496
+ tool_calls = details.get("tool_calls")
497
+ if isinstance(tool_calls, list):
498
+ payload = _approve_and_execute(
499
+ tool_calls=tool_calls,
500
+ tool_runner=shell._tool_runner,
501
+ auto_approve=bool(shell._auto_approve),
502
+ approval_state=approval_state,
503
+ prompt_fn=input,
504
+ print_fn=_stderr_print,
505
+ )
506
+ if payload is None:
507
+ print("Aborted (tool calls not executed).", file=sys.stderr)
508
+ return 1
509
+
510
+ shell._runtime.resume(
511
+ workflow=shell._agent.workflow,
512
+ run_id=run_id,
513
+ wait_key=wait.wait_key,
514
+ payload=payload,
515
+ )
516
+ continue
517
+
518
+ if isinstance(wait.prompt, str) and wait.prompt.strip() and isinstance(wait.wait_key, str) and wait.wait_key:
519
+ response = input(wait.prompt.strip() + " ")
520
+ shell._runtime.resume(
521
+ workflow=shell._agent.workflow,
522
+ run_id=run_id,
523
+ wait_key=wait.wait_key,
524
+ payload={"response": response},
525
+ )
526
+ continue
527
+
528
+ print(f"Run waiting: {wait.reason.value} ({wait.wait_key})", file=sys.stderr)
529
+ return 2
530
+
531
+ if state is None:
532
+ print("Run failed: no state produced.", file=sys.stderr)
533
+ return 1
534
+
535
+ def _pick_textish(value):
536
+ if isinstance(value, str):
537
+ return value.strip()
538
+ if value is None:
539
+ return ""
540
+ if isinstance(value, bool):
541
+ return str(value).lower()
542
+ if isinstance(value, (int, float)):
543
+ return str(value)
544
+ return ""
545
+
546
+ def _extract_answer_text(output):
547
+ if not isinstance(output, dict):
548
+ return ""
549
+ payload = output.get("result") if isinstance(output.get("result"), dict) else output
550
+ text = _pick_textish(payload.get("response"))
551
+ if not text:
552
+ text = (
553
+ _pick_textish(payload.get("answer"))
554
+ or _pick_textish(payload.get("message"))
555
+ or _pick_textish(payload.get("text"))
556
+ or _pick_textish(payload.get("content"))
557
+ )
558
+ if not text and isinstance(output.get("result"), str):
559
+ text = str(output.get("result") or "").strip()
560
+ return text
561
+
562
+ output = getattr(state, "output", None)
563
+ answer_text = _extract_answer_text(output)
564
+ if isinstance(answer_text, str) and answer_text.strip():
565
+ print(answer_text.strip())
566
+
567
+ if state.status == RunStatus.COMPLETED:
568
+ return 0
569
+
570
+ err = str(getattr(state, "error", None) or "unknown error")
571
+ print(f"Run failed: {err}", file=sys.stderr)
572
+ return 1
573
+
574
+
131
575
  def build_flow_parser() -> argparse.ArgumentParser:
132
576
  parser = argparse.ArgumentParser(
133
577
  prog="abstractcode flow",
@@ -285,8 +729,148 @@ def build_flow_parser() -> argparse.ArgumentParser:
285
729
  return parser
286
730
 
287
731
 
732
+ def build_gateway_parser() -> argparse.ArgumentParser:
733
+ parser = argparse.ArgumentParser(
734
+ prog="abstractcode gateway",
735
+ description="Run/observe workflows via AbstractGateway (HTTP control plane).",
736
+ )
737
+ sub = parser.add_subparsers(dest="command")
738
+
739
+ run = sub.add_parser("run", help="Start a new gateway run and follow it")
740
+ run.add_argument("flow_id", help="Flow id to start (or 'bundle:flow')")
741
+ run.add_argument("--bundle-id", default=None, help="Bundle id (optional if flow_id is namespaced)")
742
+ run.add_argument("--gateway-url", default=None, help="Gateway base URL (default: $ABSTRACTCODE_GATEWAY_URL)")
743
+ run.add_argument("--gateway-token", default=None, help="Gateway auth token (default: $ABSTRACTCODE_GATEWAY_TOKEN)")
744
+ run.add_argument(
745
+ "--input-json",
746
+ default=None,
747
+ help='JSON object string passed to the flow entry (e.g. \'{"prompt":"..."}\')',
748
+ )
749
+ run.add_argument(
750
+ "--input-file",
751
+ "--input-json-file",
752
+ dest="input_file",
753
+ default=None,
754
+ help="Path to a JSON file (object) passed to the flow entry",
755
+ )
756
+ run.add_argument(
757
+ "--param",
758
+ action="append",
759
+ default=[],
760
+ help="Set an input param as key=value (repeatable). Example: --param max_iterations=5",
761
+ )
762
+ run.add_argument("--no-follow", action="store_true", help="Do not tail the run; only print run_id")
763
+ run.add_argument("--poll-s", type=float, default=0.25, help="Polling interval when following (default: 0.25)")
764
+
765
+ attach = sub.add_parser("attach", help="Attach to an existing run_id and follow it")
766
+ attach.add_argument("run_id", help="Existing run_id to follow")
767
+ attach.add_argument("--gateway-url", default=None, help="Gateway base URL (default: $ABSTRACTCODE_GATEWAY_URL)")
768
+ attach.add_argument("--gateway-token", default=None, help="Gateway auth token (default: $ABSTRACTCODE_GATEWAY_TOKEN)")
769
+ attach.add_argument("--poll-s", type=float, default=0.25, help="Polling interval when following (default: 0.25)")
770
+
771
+ kg = sub.add_parser("kg", help="Query/dump the persisted KG (AbstractMemory triple store)")
772
+ kg.add_argument(
773
+ "id",
774
+ nargs="?",
775
+ default=None,
776
+ help="run_id or session_id (optional when using --scope global or --all-owners)",
777
+ )
778
+ kg.add_argument("--gateway-url", default=None, help="Gateway base URL (default: $ABSTRACTCODE_GATEWAY_URL)")
779
+ kg.add_argument("--gateway-token", default=None, help="Gateway auth token (default: $ABSTRACTCODE_GATEWAY_TOKEN)")
780
+ kg.add_argument("--scope", choices=("run", "session", "global", "all"), default="session", help="KG scope (default: session)")
781
+ kg.add_argument("--owner-id", default=None, help="Explicit owner_id override (bypasses scope owner resolution)")
782
+ kg.add_argument("--all-owners", action="store_true", help="Query across all owner_ids within the selected scope(s) (debug/audit)")
783
+ kg.add_argument("--subject", default=None, help="Filter: exact subject")
784
+ kg.add_argument("--predicate", default=None, help="Filter: exact predicate")
785
+ kg.add_argument("--object", dest="object", default=None, help="Filter: exact object")
786
+ kg.add_argument("--since", default=None, help="Filter: observed_at >= since (ISO 8601 string compare)")
787
+ kg.add_argument("--until", default=None, help="Filter: observed_at <= until (ISO 8601 string compare)")
788
+ kg.add_argument("--active-at", dest="active_at", default=None, help="Filter: valid_from/valid_until window intersection")
789
+ kg.add_argument("--query-text", dest="query_text", default=None, help="Optional semantic query text (requires embedder configured on the store)")
790
+ kg.add_argument("--min-score", dest="min_score", type=float, default=None, help="Semantic similarity threshold (0..1)")
791
+ kg.add_argument("--limit", type=int, default=0, help="Max results (default: 0 = unlimited; -1 = unlimited; positive = limit)")
792
+ kg.add_argument("--order", choices=("asc", "desc"), default="desc", help="Order by observed_at for non-semantic queries (default: desc)")
793
+ kg.add_argument(
794
+ "--format",
795
+ choices=("triples", "jsonl", "json"),
796
+ default="triples",
797
+ help="Output format: triples|jsonl|json (default: triples)",
798
+ )
799
+ kg.add_argument("--pretty", action="store_true", help="Pretty-print JSON output (json format only)")
800
+
801
+ return parser
802
+
803
+
288
804
  def main(argv: Optional[Sequence[str]] = None) -> int:
289
805
  argv_list = list(argv) if argv is not None else sys.argv[1:]
806
+ _configure_abstractcode_logging(argv_list)
807
+
808
+ if argv_list and argv_list[0] == "gateway":
809
+ parser = build_gateway_parser()
810
+ args, unknown = parser.parse_known_args(argv_list[1:])
811
+ from .gateway_cli import attach_gateway_run_command, query_gateway_kg_command, run_gateway_flow_command
812
+
813
+ cmd = getattr(args, "command", None)
814
+ if cmd == "run":
815
+ from .flow_cli import _parse_input_json, _parse_kv_list, _parse_unknown_params
816
+
817
+ input_data = _parse_input_json(raw_json=args.input_json, json_path=args.input_file)
818
+ input_data.update(_parse_kv_list(list(getattr(args, "param", []) or [])))
819
+ # Allow unknown args to be interpreted as params (same as `flow run`).
820
+ input_data.update(_parse_unknown_params(list(unknown or [])))
821
+
822
+ run_gateway_flow_command(
823
+ gateway_url=args.gateway_url,
824
+ gateway_token=args.gateway_token,
825
+ flow_id=str(args.flow_id),
826
+ bundle_id=str(args.bundle_id).strip() if isinstance(args.bundle_id, str) and str(args.bundle_id).strip() else None,
827
+ input_data=input_data,
828
+ follow=not bool(getattr(args, "no_follow", False)),
829
+ poll_s=float(getattr(args, "poll_s", 0.25) or 0.25),
830
+ )
831
+ return 0
832
+
833
+ if cmd == "attach":
834
+ if unknown:
835
+ parser.error(f"Unknown arguments: {' '.join(unknown)}")
836
+ attach_gateway_run_command(
837
+ gateway_url=args.gateway_url,
838
+ gateway_token=args.gateway_token,
839
+ run_id=str(args.run_id),
840
+ follow=True,
841
+ poll_s=float(getattr(args, "poll_s", 0.25) or 0.25),
842
+ )
843
+ return 0
844
+
845
+ if cmd == "kg":
846
+ if unknown:
847
+ parser.error(f"Unknown arguments: {' '.join(unknown)}")
848
+ id_raw = getattr(args, "id", None)
849
+ id_value = str(id_raw).strip() if isinstance(id_raw, str) and str(id_raw).strip() else None
850
+ query_gateway_kg_command(
851
+ gateway_url=args.gateway_url,
852
+ gateway_token=args.gateway_token,
853
+ run_id=id_value,
854
+ scope=str(args.scope),
855
+ owner_id=getattr(args, "owner_id", None),
856
+ all_owners=bool(getattr(args, "all_owners", False)),
857
+ subject=getattr(args, "subject", None),
858
+ predicate=getattr(args, "predicate", None),
859
+ object_value=getattr(args, "object", None),
860
+ since=getattr(args, "since", None),
861
+ until=getattr(args, "until", None),
862
+ active_at=getattr(args, "active_at", None),
863
+ query_text=getattr(args, "query_text", None),
864
+ min_score=getattr(args, "min_score", None),
865
+ limit=int(getattr(args, "limit", 0)),
866
+ order=str(getattr(args, "order", "desc") or "desc"),
867
+ fmt=str(getattr(args, "format", "triples") or "triples"),
868
+ pretty=bool(getattr(args, "pretty", False)),
869
+ )
870
+ return 0
871
+
872
+ build_gateway_parser().print_help()
873
+ return 2
290
874
 
291
875
  if argv_list and argv_list[0] == "flow":
292
876
  parser = build_flow_parser()
@@ -376,9 +960,94 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
376
960
  build_flow_parser().print_help()
377
961
  return 2
378
962
 
963
+ if argv_list and argv_list[0] == "workflow":
964
+ parser = build_workflow_parser()
965
+ args, unknown = parser.parse_known_args(argv_list[1:])
966
+ if unknown:
967
+ parser.error(f"Unknown arguments: {' '.join(unknown)}")
968
+ from .workflow_cli import (
969
+ deprecate_workflow_bundle_command,
970
+ install_workflow_bundle_command,
971
+ list_workflow_bundles_command,
972
+ remove_workflow_bundle_command,
973
+ undeprecate_workflow_bundle_command,
974
+ workflow_bundle_info_command,
975
+ )
976
+
977
+ cmd = getattr(args, "command", None)
978
+ if cmd == "install":
979
+ install_workflow_bundle_command(
980
+ source=str(args.source),
981
+ gateway_url=getattr(args, "gateway_url", None),
982
+ gateway_token=getattr(args, "gateway_token", None),
983
+ overwrite=bool(getattr(args, "overwrite", False)),
984
+ output_json=bool(getattr(args, "json", False)),
985
+ )
986
+ return 0
987
+ if cmd == "list":
988
+ list_workflow_bundles_command(
989
+ gateway_url=getattr(args, "gateway_url", None),
990
+ gateway_token=getattr(args, "gateway_token", None),
991
+ interface=getattr(args, "interface", None),
992
+ all_versions=bool(getattr(args, "all", False)),
993
+ include_deprecated=bool(getattr(args, "include_deprecated", False)),
994
+ output_json=bool(getattr(args, "json", False)),
995
+ )
996
+ return 0
997
+ if cmd == "info":
998
+ workflow_bundle_info_command(
999
+ bundle_ref=str(args.bundle),
1000
+ gateway_url=getattr(args, "gateway_url", None),
1001
+ gateway_token=getattr(args, "gateway_token", None),
1002
+ output_json=bool(getattr(args, "json", False)),
1003
+ )
1004
+ return 0
1005
+ if cmd == "remove":
1006
+ remove_workflow_bundle_command(
1007
+ bundle_ref=str(args.bundle),
1008
+ gateway_url=getattr(args, "gateway_url", None),
1009
+ gateway_token=getattr(args, "gateway_token", None),
1010
+ output_json=bool(getattr(args, "json", False)),
1011
+ )
1012
+ return 0
1013
+
1014
+ if cmd == "deprecate":
1015
+ deprecate_workflow_bundle_command(
1016
+ bundle_id=str(args.bundle),
1017
+ flow_id=getattr(args, "flow_id", None),
1018
+ reason=getattr(args, "reason", None),
1019
+ gateway_url=getattr(args, "gateway_url", None),
1020
+ gateway_token=getattr(args, "gateway_token", None),
1021
+ output_json=bool(getattr(args, "json", False)),
1022
+ )
1023
+ return 0
1024
+
1025
+ if cmd == "undeprecate":
1026
+ undeprecate_workflow_bundle_command(
1027
+ bundle_id=str(args.bundle),
1028
+ flow_id=getattr(args, "flow_id", None),
1029
+ gateway_url=getattr(args, "gateway_url", None),
1030
+ gateway_token=getattr(args, "gateway_token", None),
1031
+ output_json=bool(getattr(args, "json", False)),
1032
+ )
1033
+ return 0
1034
+
1035
+ build_workflow_parser().print_help()
1036
+ return 2
1037
+
379
1038
  args = build_agent_parser().parse_args(argv_list)
380
1039
  state_file = None if args.no_state else args.state_file
381
1040
 
1041
+ # Best-effort: pass gateway settings to the TUI via env vars (not persisted).
1042
+ gw_url = getattr(args, "gateway_url", None)
1043
+ if isinstance(gw_url, str) and gw_url.strip():
1044
+ os.environ["ABSTRACTCODE_GATEWAY_URL"] = gw_url.strip()
1045
+ gw_token = getattr(args, "gateway_token", None)
1046
+ if isinstance(gw_token, str) and gw_token.strip():
1047
+ os.environ["ABSTRACTCODE_GATEWAY_TOKEN"] = gw_token.strip()
1048
+
1049
+ from .react_shell import ReactShell
1050
+
382
1051
  shell = ReactShell(
383
1052
  agent=str(args.agent),
384
1053
  provider=args.provider,
@@ -393,6 +1062,16 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
393
1062
  max_tokens=args.max_tokens,
394
1063
  color=not bool(args.no_color),
395
1064
  )
1065
+
1066
+ prompt = getattr(args, "prompt", None)
1067
+ if isinstance(prompt, str) and prompt.strip():
1068
+ if state_file:
1069
+ try:
1070
+ shell._try_load_state()
1071
+ except Exception:
1072
+ pass
1073
+ return _run_one_shot_prompt(shell=shell, prompt=prompt)
1074
+
396
1075
  shell.run()
397
1076
  return 0
398
1077