agentproc 0.1.1__tar.gz → 0.3.0__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,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentproc
3
- Version: 0.1.1
4
- Summary: AgentProc SDK — connect any Agent CLI to a messaging platform with a single function
3
+ Version: 0.3.0
4
+ Summary: AgentProc Protocol SDK + CLI — connect any Agent CLI to a messaging platform
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/jeffkit/agentproc
7
- Project-URL: Documentation, https://jeffkit.github.io/agentproc/
7
+ Project-URL: Documentation, https://agentproc.dev/
8
8
  Project-URL: Repository, https://github.com/jeffkit/agentproc
9
9
  Project-URL: Issues, https://github.com/jeffkit/agentproc/issues
10
10
  Requires-Python: >=3.9
@@ -4,16 +4,19 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentproc"
7
- version = "0.1.1"
8
- description = "AgentProc SDK — connect any Agent CLI to a messaging platform with a single function"
7
+ version = "0.3.0"
8
+ description = "AgentProc Protocol SDK + CLI — connect any Agent CLI to a messaging platform"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.9"
12
12
  dependencies = []
13
13
 
14
+ [project.scripts]
15
+ agentproc = "agentproc.cli:main"
16
+
14
17
  [project.urls]
15
18
  Homepage = "https://github.com/jeffkit/agentproc"
16
- Documentation = "https://jeffkit.github.io/agentproc/"
19
+ Documentation = "https://agentproc.dev/"
17
20
  Repository = "https://github.com/jeffkit/agentproc"
18
21
  Issues = "https://github.com/jeffkit/agentproc/issues"
19
22
 
@@ -47,8 +47,35 @@ __all__ = [
47
47
  "load_history",
48
48
  "append_history",
49
49
  "session_file_path",
50
+ "__version__",
50
51
  ]
51
52
 
53
+
54
+ def _read_version() -> str:
55
+ try:
56
+ from importlib.metadata import version, PackageNotFoundError
57
+ try:
58
+ return version("agentproc")
59
+ except PackageNotFoundError:
60
+ pass
61
+ except ImportError:
62
+ pass
63
+ try:
64
+ from pathlib import Path
65
+ toml_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
66
+ if toml_path.exists():
67
+ import re
68
+ text = toml_path.read_text(encoding="utf-8")
69
+ m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
70
+ if m:
71
+ return m.group(1)
72
+ except Exception:
73
+ pass
74
+ return "0.0.0+unknown"
75
+
76
+
77
+ __version__ = _read_version()
78
+
52
79
  PROTOCOL_VERSION = "0.1"
53
80
 
54
81
 
@@ -0,0 +1,504 @@
1
+ """agentproc CLI — run any AgentProc profile against a message.
2
+
3
+ Usage:
4
+ agentproc --profile <path.yaml> --prompt "hello" [options]
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional, Tuple
16
+
17
+ from .runner import (
18
+ PROTOCOL_VERSION,
19
+ EXIT_ERROR,
20
+ EXIT_SUCCESS,
21
+ EXIT_TIMEOUT,
22
+ RunOptions,
23
+ run,
24
+ )
25
+
26
+
27
+ def _read_pkg_version() -> str:
28
+ """Read installed package version.
29
+
30
+ Primary: importlib.metadata (works after pip/pipx install).
31
+ Fallback: parse pyproject.toml (works in source checkout, dev mode).
32
+ """
33
+ try:
34
+ from importlib.metadata import version, PackageNotFoundError
35
+ try:
36
+ return version("agentproc")
37
+ except PackageNotFoundError:
38
+ pass
39
+ except ImportError:
40
+ pass
41
+ # Fallback for source checkout (development, not installed).
42
+ try:
43
+ toml_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
44
+ if toml_path.exists():
45
+ text = toml_path.read_text(encoding="utf-8")
46
+ m = re.search(r'^version\s*=\s*"([^"]+)"', text, re.MULTILINE)
47
+ if m:
48
+ return m.group(1)
49
+ except Exception:
50
+ pass
51
+ return "0.0.0+unknown"
52
+
53
+
54
+ PKG_VERSION = _read_pkg_version()
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Minimal YAML parser (zero-dep; covers hub profile subset)
59
+ # ---------------------------------------------------------------------------
60
+
61
+ def parse_yaml(text: str) -> Dict[str, Any]:
62
+ """Parse a YAML subset into a Python dict.
63
+
64
+ Supports: nested maps, block scalars (|), inline flow sequences ([a, b]),
65
+ block sequences (- item), quoted strings, booleans, null, ints, floats.
66
+ """
67
+ text_stripped = text.strip()
68
+ if text_stripped.startswith("{") or text_stripped.startswith("["):
69
+ try:
70
+ return json.loads(text)
71
+ except Exception:
72
+ pass
73
+
74
+ lines = text.splitlines()
75
+ root: Dict[str, Any] = {}
76
+ stack: List[Tuple[int, Any]] = [(-1, root)]
77
+
78
+ def current(min_indent: int) -> Tuple[int, Any]:
79
+ while len(stack) > 1 and stack[-1][0] >= min_indent:
80
+ stack.pop()
81
+ return stack[-1]
82
+
83
+ i = 0
84
+ while i < len(lines):
85
+ raw = lines[i]
86
+ if raw.strip() == "" or raw.strip().startswith("#"):
87
+ i += 1
88
+ continue
89
+ indent = _leading_spaces(raw)
90
+ content = raw[indent:].rstrip("\r")
91
+ _, container = current(indent)
92
+
93
+ if content == "-" or content.startswith("- "):
94
+ if isinstance(container, list):
95
+ rest = "" if content == "-" else content[2:]
96
+ if rest.strip():
97
+ container.append(_strip_scalar(rest))
98
+ i += 1
99
+ continue
100
+
101
+ m = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$", content)
102
+ if not m:
103
+ i += 1
104
+ continue
105
+ key, val = m.group(1), m.group(2)
106
+
107
+ if val == "":
108
+ j = i + 1
109
+ while j < len(lines) and (lines[j].strip() == "" or lines[j].strip().startswith("#")):
110
+ j += 1
111
+ if j < len(lines):
112
+ next_indent = _leading_spaces(lines[j])
113
+ next_content = lines[j][next_indent:]
114
+ if next_indent > indent and (next_content == "-" or next_content.startswith("- ")):
115
+ arr: List[Any] = []
116
+ if isinstance(container, dict):
117
+ container[key] = arr
118
+ stack.append((indent, arr))
119
+ i += 1
120
+ continue
121
+ elif next_indent > indent:
122
+ child: Dict[str, Any] = {}
123
+ if isinstance(container, dict):
124
+ container[key] = child
125
+ stack.append((indent, child))
126
+ i += 1
127
+ continue
128
+ if isinstance(container, dict):
129
+ container[key] = ""
130
+ i += 1
131
+ continue
132
+
133
+ if val in ("|", "|-", ">"):
134
+ block_lines: List[str] = []
135
+ j = i + 1
136
+ while j < len(lines):
137
+ nr = lines[j]
138
+ if nr.strip() == "":
139
+ block_lines.append("")
140
+ j += 1
141
+ continue
142
+ ni = _leading_spaces(nr)
143
+ if ni <= indent:
144
+ break
145
+ block_lines.append(nr[min(indent + 2, len(nr)):])
146
+ j += 1
147
+ joined = "\n".join(block_lines)
148
+ if val == "|":
149
+ value = re.sub(r"\n*$", "\n", joined)
150
+ else:
151
+ value = re.sub(r"\n*$", "", joined)
152
+ if isinstance(container, dict):
153
+ container[key] = value
154
+ i = j
155
+ continue
156
+
157
+ if isinstance(container, dict):
158
+ container[key] = _strip_scalar(val)
159
+ i += 1
160
+
161
+ return root
162
+
163
+
164
+ def _leading_spaces(s: str) -> int:
165
+ n = 0
166
+ for ch in s:
167
+ if ch == " ":
168
+ n += 1
169
+ elif ch == "\t":
170
+ n += 2
171
+ else:
172
+ break
173
+ return n
174
+
175
+
176
+ def _strip_scalar(v: str) -> Any:
177
+ v = v.strip()
178
+ if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
179
+ return v[1:-1]
180
+ if v.startswith("[") and v.endswith("]"):
181
+ inner = v[1:-1].strip()
182
+ if not inner:
183
+ return []
184
+ return [_strip_scalar(s.strip()) for s in inner.split(",")]
185
+ lv = v.lower()
186
+ if lv == "true":
187
+ return True
188
+ if lv == "false":
189
+ return False
190
+ if lv in ("null", "~", ""):
191
+ return None
192
+ if re.match(r"^[+-]?\d+$", v):
193
+ return int(v)
194
+ if re.match(r"^[+-]?\d+\.\d+$", v):
195
+ return float(v)
196
+ return v
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Argparse setup
201
+ # ---------------------------------------------------------------------------
202
+
203
+ def build_parser() -> argparse.ArgumentParser:
204
+ p = argparse.ArgumentParser(
205
+ prog="agentproc",
206
+ description="Run an AgentProc profile against a message.",
207
+ add_help=False,
208
+ )
209
+ p.add_argument("-h", "--help", action="store_true")
210
+ p.add_argument("--version", action="store_true")
211
+ p.add_argument("-p", "--profile")
212
+ p.add_argument("--prompt")
213
+ p.add_argument("--session", default="")
214
+ p.add_argument("--session-name", default="default")
215
+ p.add_argument("--from", dest="from_user", default="")
216
+ p.add_argument("--cwd")
217
+ p.add_argument("--env", action="append", default=[])
218
+ p.add_argument("--timeout", type=int)
219
+ p.add_argument("--no-stream", action="store_true")
220
+ p.add_argument("--verbose", action="store_true")
221
+ p.add_argument("--quiet", action="store_true")
222
+ p.add_argument("--raw", action="store_true")
223
+ p.add_argument("--stdin", action="store_true")
224
+ return p
225
+
226
+
227
+ def show_help() -> None:
228
+ sys.stdout.write(f"""agentproc v{PKG_VERSION} (protocol {PROTOCOL_VERSION})
229
+
230
+ Usage:
231
+ agentproc --profile <path.yaml> --prompt "hello" [options]
232
+
233
+ Required:
234
+ --profile, -p <path> Profile YAML path
235
+ --prompt <text> User message (or use --stdin)
236
+
237
+ Session:
238
+ --session <id> Previous session id (multi-turn)
239
+ --session-name <name> Human-readable session name (default: "default")
240
+ --from <user> Sender identifier
241
+
242
+ Execution:
243
+ --cwd <path> Override profile.cwd
244
+ --env KEY=VALUE Extra env var (repeatable)
245
+ --timeout <secs> Override profile.timeout_secs
246
+ --no-stream Set AGENT_STREAMING=0
247
+
248
+ Output:
249
+ --verbose Forward protocol lines to stderr (default)
250
+ --quiet Suppress protocol lines on stderr
251
+ --raw Don't parse stdout; forward agent output verbatim
252
+ --stdin Read prompt from stdin instead of --prompt
253
+
254
+ Other:
255
+ --version Print version and exit
256
+ --help, -h Show this help
257
+
258
+ Output semantics:
259
+ stderr → protocol lines (AGENT_PARTIAL:, AGENT_SESSION:, AGENT_ERROR:)
260
+ stdout → final reply body (non-protocol lines)
261
+ exit → 0 success · 1 error · 124 timeout (per spec)
262
+
263
+ The final session id is printed on stderr as: agentproc:session:<id>
264
+
265
+ Examples:
266
+ agentproc --profile hub/echo-agent/profile.yaml --prompt "hi"
267
+ agentproc -p hub/claude-code/profile.yaml --prompt "hello" --verbose
268
+ cat prompt.txt | agentproc -p prof.yaml --stdin
269
+ """)
270
+
271
+
272
+ def show_version() -> None:
273
+ sys.stdout.write(f"agentproc {PKG_VERSION} (protocol {PROTOCOL_VERSION})\n")
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Hub subcommand dispatcher
278
+ # ---------------------------------------------------------------------------
279
+
280
+ def _run_hub_subcommand(args: List[str]) -> int:
281
+ """Handle `agentproc hub <list|show|install|run> [args]`."""
282
+ from . import hub as hub_mod
283
+
284
+ if not args or args[0] in ("-h", "--help"):
285
+ _show_hub_help()
286
+ return 0
287
+
288
+ sub = args[0]
289
+ rest = args[1:]
290
+
291
+ # Common flag
292
+ refresh = "--refresh" in rest
293
+ positional = [a for a in rest if not a.startswith("--")]
294
+
295
+ def _log(msg: str) -> None:
296
+ sys.stderr.write(msg + "\n")
297
+
298
+ if sub == "list":
299
+ profiles = hub_mod.list_profiles(on_log=_log)
300
+ sys.stdout.write("Available profiles in the official hub:\n\n")
301
+ for p in profiles:
302
+ sys.stdout.write(
303
+ f" {p['name']:<15} {p['tested']:<12} {p['description'][:60]}\n"
304
+ )
305
+ sys.stdout.write('\nRun `agentproc hub run <name> -p "hi"` to use one.\n')
306
+ return 0
307
+
308
+ if sub == "show":
309
+ if not positional:
310
+ sys.stderr.write("error: hub show requires a profile name\n")
311
+ return 2
312
+ readme = hub_mod.show_readme(positional[0], refresh=refresh, on_log=_log)
313
+ sys.stdout.write(readme)
314
+ if not readme.endswith("\n"):
315
+ sys.stdout.write("\n")
316
+ return 0
317
+
318
+ if sub == "install":
319
+ if not positional:
320
+ sys.stderr.write("error: hub install requires a profile name\n")
321
+ return 2
322
+ hub_mod.install_profile(positional[0], Path.cwd(), refresh=refresh, on_log=_log)
323
+ return 0
324
+
325
+ if sub == "run":
326
+ if not positional:
327
+ sys.stderr.write("error: hub run requires a profile name\n")
328
+ return 2
329
+ profile_name = positional[0]
330
+ cache_dir = hub_mod.fetch_profile(profile_name, refresh=refresh, on_log=_log)
331
+ profile_path = str(cache_dir / "profile.yaml")
332
+
333
+ # Re-parse remaining args (excluding the profile name) as runner options.
334
+ parser = build_parser()
335
+ # Drop the first positional (profile name) and --refresh from rest.
336
+ runner_args = [a for i, a in enumerate(rest) if not (
337
+ (not a.startswith("--") and i == 0) or a == "--refresh"
338
+ )]
339
+ opts = parser.parse_args(runner_args)
340
+
341
+ if not opts.prompt and not opts.stdin:
342
+ sys.stderr.write("error: hub run requires --prompt <text> or --stdin\n")
343
+ return 2
344
+
345
+ return _run_agent_with_profile(profile_path, opts)
346
+
347
+ sys.stderr.write(f"error: unknown hub subcommand: {sub}\n\n")
348
+ _show_hub_help()
349
+ return 2
350
+
351
+
352
+ def _show_hub_help() -> None:
353
+ sys.stdout.write("""agentproc hub — manage profiles from the official Hub
354
+
355
+ Usage:
356
+ agentproc hub list List all profiles in the hub
357
+ agentproc hub show <name> Show a profile's README
358
+ agentproc hub install <name> Copy a profile to the current directory
359
+ agentproc hub run <name> [run-options] Fetch (if needed) and run a profile
360
+
361
+ Hub run options (same as the regular --profile runner):
362
+ -p, --prompt <text> User message (or use --stdin)
363
+ --cwd <path> Override profile.cwd (default: current dir)
364
+ --env KEY=VALUE Extra env var (repeatable)
365
+ --session <id> Previous session id for multi-turn
366
+ --timeout <secs> Override profile.timeout_secs
367
+ --no-stream Disable streaming
368
+ --verbose / --quiet Protocol line visibility (default: verbose)
369
+ --stdin Read prompt from stdin
370
+
371
+ Common options:
372
+ --refresh Force re-fetch from GitHub (ignore cache)
373
+ -h, --help Show this help
374
+
375
+ Examples:
376
+ agentproc hub list
377
+ agentproc hub run echo-agent -p "hello"
378
+ cd ~/projects/my-app && agentproc hub run claude-code -p "explain this" --env ANTHROPIC_API_KEY=$KEY
379
+ agentproc hub show codex
380
+ agentproc hub install agy
381
+
382
+ Profiles are cached at ~/.agentproc/cache/hub/<name>/ (24h TTL).
383
+ """)
384
+
385
+
386
+ def _run_agent_with_profile(profile_path: str, opts) -> int:
387
+ """Shared runner logic for both --profile path and hub run path."""
388
+ try:
389
+ yaml_text = Path(profile_path).resolve().read_text(encoding="utf-8")
390
+ profile_raw = parse_yaml(yaml_text)
391
+ except FileNotFoundError:
392
+ sys.stderr.write(f"error: profile not found: {profile_path}\n")
393
+ return 2
394
+ except Exception as e:
395
+ sys.stderr.write(f"error: failed to parse profile {profile_path}: {e}\n")
396
+ return 2
397
+
398
+ # Read prompt.
399
+ prompt = opts.prompt
400
+ if opts.stdin:
401
+ try:
402
+ prompt = sys.stdin.read().rstrip("\n")
403
+ except KeyboardInterrupt:
404
+ return 1
405
+ if prompt is None:
406
+ sys.stderr.write("error: --prompt (or --stdin) is required\n")
407
+ return 2
408
+
409
+ extra_env: Dict[str, str] = {}
410
+ for kv in opts.env or []:
411
+ eq = kv.find("=")
412
+ if eq < 0:
413
+ sys.stderr.write(f"error: --env expects KEY=VALUE, got: {kv}\n")
414
+ return 2
415
+ extra_env[kv[:eq]] = kv[eq + 1:]
416
+
417
+ streaming = False if opts.no_stream else None
418
+
419
+ if opts.raw:
420
+ r = run(
421
+ profile_raw,
422
+ RunOptions(
423
+ message=prompt,
424
+ session_id=opts.session,
425
+ session_name=opts.session_name,
426
+ from_user=opts.from_user,
427
+ streaming=streaming,
428
+ cwd=opts.cwd,
429
+ extra_env=extra_env,
430
+ timeout_secs=opts.timeout,
431
+ ),
432
+ )
433
+ sys.stdout.write(r.reply)
434
+ if r.reply and not r.reply.endswith("\n"):
435
+ sys.stdout.write("\n")
436
+ return 0 if r.exit_code == 0 else 1
437
+
438
+ verbose = opts.verbose or not opts.quiet
439
+
440
+ r = run(
441
+ profile_raw,
442
+ RunOptions(
443
+ message=prompt,
444
+ session_id=opts.session,
445
+ session_name=opts.session_name,
446
+ from_user=opts.from_user,
447
+ streaming=streaming,
448
+ cwd=opts.cwd,
449
+ extra_env=extra_env,
450
+ timeout_secs=opts.timeout,
451
+ on_partial=lambda t: verbose and sys.stderr.write(
452
+ f"AGENT_PARTIAL:{json.dumps(t, ensure_ascii=False)}\n"
453
+ ),
454
+ on_session=lambda sid: verbose and sys.stderr.write(f"AGENT_SESSION:{sid}\n"),
455
+ on_error=lambda msg: verbose and sys.stderr.write(
456
+ f"AGENT_ERROR:{json.dumps(msg, ensure_ascii=False)}\n"
457
+ ),
458
+ on_stderr=lambda line: verbose and sys.stderr.write(f"[agent stderr] {line}\n"),
459
+ ),
460
+ )
461
+
462
+ if r.reply:
463
+ sys.stdout.write(r.reply)
464
+ if not r.reply.endswith("\n"):
465
+ sys.stdout.write("\n")
466
+ if r.session_id:
467
+ sys.stderr.write(f"agentproc:session:{r.session_id}\n")
468
+ if r.error:
469
+ sys.stderr.write(f"agentproc:error:{r.error}\n")
470
+ return 0 if r.exit_code == 0 else 1
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # Main
475
+ # ---------------------------------------------------------------------------
476
+
477
+ def main(argv: Optional[List[str]] = None) -> int:
478
+ if argv is None:
479
+ argv = sys.argv[1:]
480
+
481
+ # `agentproc hub <subcommand>` — defer to hub dispatcher.
482
+ if argv and argv[0] == "hub":
483
+ return _run_hub_subcommand(argv[1:])
484
+
485
+ parser = build_parser()
486
+ opts = parser.parse_args(argv)
487
+
488
+ if opts.help:
489
+ show_help()
490
+ return 0
491
+ if opts.version:
492
+ show_version()
493
+ return 0
494
+
495
+ if not opts.profile:
496
+ sys.stderr.write("error: --profile is required\n\n")
497
+ show_help()
498
+ return 2
499
+
500
+ return _run_agent_with_profile(opts.profile, opts)
501
+
502
+
503
+ if __name__ == "__main__":
504
+ sys.exit(main())