jdsl 0.6.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.
- jdsl/__init__.py +116 -0
- jdsl/cli.py +387 -0
- jdsl/config.py +80 -0
- jdsl/context.py +159 -0
- jdsl/dsl.py +174 -0
- jdsl/ir/__init__.py +63 -0
- jdsl/ir/expr.py +193 -0
- jdsl/ir/lower.py +160 -0
- jdsl/ir/schema.py +293 -0
- jdsl/ir/validate.py +134 -0
- jdsl/package/__init__.py +38 -0
- jdsl/package/authoring.py +244 -0
- jdsl/package/export.py +129 -0
- jdsl/package/load.py +155 -0
- jdsl/package/manifest.py +120 -0
- jdsl/package/proposal.py +344 -0
- jdsl/provider.py +214 -0
- jdsl/render.py +35 -0
- jdsl/router.py +31 -0
- jdsl/trace/__init__.py +41 -0
- jdsl/trace/blobs.py +56 -0
- jdsl/trace/events.py +180 -0
- jdsl/trace/jsonl.py +72 -0
- jdsl/trace/redaction.py +89 -0
- jdsl/trace/replay.py +117 -0
- jdsl/trace/sink.py +110 -0
- jdsl/tree.py +753 -0
- jdsl/tui/__init__.py +6 -0
- jdsl/tui/app.py +164 -0
- jdsl/tui/commands.py +60 -0
- jdsl/tui/definitions.py +157 -0
- jdsl/tui/dialogs.py +56 -0
- jdsl/tui/editors.py +420 -0
- jdsl/tui/run_inputs.py +113 -0
- jdsl/tui/run_panel.py +69 -0
- jdsl/tui/screens.py +189 -0
- jdsl/tui/settings.py +67 -0
- jdsl/tui/signature_editor.py +325 -0
- jdsl/tui/templates.py +387 -0
- jdsl/tui/theme.py +33 -0
- jdsl/tui/workbench.py +1457 -0
- jdsl/tui/workbench.tcss +181 -0
- jdsl/tui_skill.py +5 -0
- jdsl-0.6.2.dist-info/METADATA +231 -0
- jdsl-0.6.2.dist-info/RECORD +73 -0
- jdsl-0.6.2.dist-info/WHEEL +4 -0
- jdsl-0.6.2.dist-info/entry_points.txt +2 -0
- jdsl-0.6.2.dist-info/licenses/LICENSE +8 -0
- jdsl_harness/__init__.py +8 -0
- jdsl_harness/adapters/__init__.py +6 -0
- jdsl_harness/adapters/claude_code.py +144 -0
- jdsl_harness/adapters/correlation.py +88 -0
- jdsl_harness/adapters/gemini_cli.py +70 -0
- jdsl_harness/adapters/generic_mcp.py +53 -0
- jdsl_harness/adapters/import_jsonl.py +86 -0
- jdsl_harness/adapters/opencode.py +110 -0
- jdsl_harness/capture.py +129 -0
- jdsl_harness/compiler/__init__.py +48 -0
- jdsl_harness/compiler/candidates.py +131 -0
- jdsl_harness/compiler/consolidate.py +141 -0
- jdsl_harness/compiler/lineage.py +106 -0
- jdsl_harness/compiler/model.py +175 -0
- jdsl_harness/compiler/normalize.py +157 -0
- jdsl_harness/compiler/package.py +125 -0
- jdsl_harness/compiler/residualize.py +31 -0
- jdsl_harness/compiler/staticize.py +258 -0
- jdsl_harness/compiler/verify.py +127 -0
- jdsl_harness/gateway.py +107 -0
- jdsl_harness/mcp_proxy.py +277 -0
- jdsl_harness/metrics.py +125 -0
- jdsl_harness/server.py +260 -0
- jdsl_harness/state.py +64 -0
- jdsl_harness/store.py +222 -0
jdsl/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""jdsl — behavior-tree agents with DSPy-style signatures, run by a tree-walking
|
|
2
|
+
interpreter. Authoring surface: tool, root, seq, sel, repeat, act, check, predict, ref, store."""
|
|
3
|
+
|
|
4
|
+
from jdsl.context import Blackboard, ContextWindow, ModelTurn, Ref, RunContext, ToolCall, Write
|
|
5
|
+
from jdsl.dsl import (
|
|
6
|
+
act,
|
|
7
|
+
check,
|
|
8
|
+
guard,
|
|
9
|
+
guard_call,
|
|
10
|
+
invert,
|
|
11
|
+
oneshot,
|
|
12
|
+
optional,
|
|
13
|
+
predict,
|
|
14
|
+
react,
|
|
15
|
+
ref,
|
|
16
|
+
repeat,
|
|
17
|
+
root,
|
|
18
|
+
sel,
|
|
19
|
+
seq,
|
|
20
|
+
store,
|
|
21
|
+
timeout,
|
|
22
|
+
tool,
|
|
23
|
+
)
|
|
24
|
+
from jdsl.provider import LanguageModel
|
|
25
|
+
from jdsl.render import render
|
|
26
|
+
from jdsl.trace import (
|
|
27
|
+
BlobStore,
|
|
28
|
+
EventKind,
|
|
29
|
+
EventSource,
|
|
30
|
+
JsonlTraceSink,
|
|
31
|
+
ListTraceSink,
|
|
32
|
+
NullTraceSink,
|
|
33
|
+
Redactor,
|
|
34
|
+
SafeSink,
|
|
35
|
+
TraceEvent,
|
|
36
|
+
read_events,
|
|
37
|
+
segment_episodes,
|
|
38
|
+
)
|
|
39
|
+
from jdsl.tree import (
|
|
40
|
+
Action,
|
|
41
|
+
Check,
|
|
42
|
+
Decorator,
|
|
43
|
+
Guard,
|
|
44
|
+
GuardCall,
|
|
45
|
+
Invert,
|
|
46
|
+
Node,
|
|
47
|
+
OneShot,
|
|
48
|
+
Optional,
|
|
49
|
+
Predict,
|
|
50
|
+
React,
|
|
51
|
+
Repeat,
|
|
52
|
+
Root,
|
|
53
|
+
Selector,
|
|
54
|
+
Sequence,
|
|
55
|
+
Status,
|
|
56
|
+
Timeout,
|
|
57
|
+
assign_runtime_ids,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"tool",
|
|
62
|
+
"act",
|
|
63
|
+
"seq",
|
|
64
|
+
"sel",
|
|
65
|
+
"repeat",
|
|
66
|
+
"check",
|
|
67
|
+
"guard",
|
|
68
|
+
"guard_call",
|
|
69
|
+
"predict",
|
|
70
|
+
"react",
|
|
71
|
+
"invert",
|
|
72
|
+
"optional",
|
|
73
|
+
"timeout",
|
|
74
|
+
"oneshot",
|
|
75
|
+
"root",
|
|
76
|
+
"store",
|
|
77
|
+
"ref",
|
|
78
|
+
"render",
|
|
79
|
+
"Ref",
|
|
80
|
+
"Status",
|
|
81
|
+
"Node",
|
|
82
|
+
"Action",
|
|
83
|
+
"Check",
|
|
84
|
+
"Guard",
|
|
85
|
+
"GuardCall",
|
|
86
|
+
"Repeat",
|
|
87
|
+
"Sequence",
|
|
88
|
+
"Selector",
|
|
89
|
+
"Predict",
|
|
90
|
+
"React",
|
|
91
|
+
"Decorator",
|
|
92
|
+
"Invert",
|
|
93
|
+
"Optional",
|
|
94
|
+
"Timeout",
|
|
95
|
+
"OneShot",
|
|
96
|
+
"Root",
|
|
97
|
+
"LanguageModel",
|
|
98
|
+
"RunContext",
|
|
99
|
+
"Blackboard",
|
|
100
|
+
"ContextWindow",
|
|
101
|
+
"ToolCall",
|
|
102
|
+
"ModelTurn",
|
|
103
|
+
"Write",
|
|
104
|
+
"assign_runtime_ids",
|
|
105
|
+
"TraceEvent",
|
|
106
|
+
"EventKind",
|
|
107
|
+
"EventSource",
|
|
108
|
+
"NullTraceSink",
|
|
109
|
+
"ListTraceSink",
|
|
110
|
+
"JsonlTraceSink",
|
|
111
|
+
"SafeSink",
|
|
112
|
+
"BlobStore",
|
|
113
|
+
"Redactor",
|
|
114
|
+
"read_events",
|
|
115
|
+
"segment_episodes",
|
|
116
|
+
]
|
jdsl/cli.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""The jdsl CLI: runtime (`config`, `run`, `show`) plus the harness/compiler
|
|
2
|
+
control surface (`capture`, `compile`, `package`, `harness`) from design §37.
|
|
3
|
+
|
|
4
|
+
Harness commands import `jdsl_harness` lazily, so the dependency-light runtime core
|
|
5
|
+
keeps working when the harness extra is not installed."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from jdsl import config
|
|
19
|
+
from jdsl.render import render
|
|
20
|
+
from jdsl.tree import Root
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
add_completion=False,
|
|
24
|
+
help=("Declarative behavior-tree agents. Read docs at "
|
|
25
|
+
"https://cantor-industries.github.io/jdsl-py/ to understand what jdsl is."),
|
|
26
|
+
)
|
|
27
|
+
config_app = typer.Typer(help="Manage provider API keys (~/.local/share/recon/auth.json).")
|
|
28
|
+
app.add_typer(config_app, name="config")
|
|
29
|
+
|
|
30
|
+
package_app = typer.Typer(help="Inspect, verify, and run compiled .jdsl behavior packages.")
|
|
31
|
+
capture_app = typer.Typer(help="List and inspect captured trace sets.")
|
|
32
|
+
harness_app = typer.Typer(help="Run the local harness daemon (ingest + control plane).")
|
|
33
|
+
app.add_typer(package_app, name="package")
|
|
34
|
+
app.add_typer(capture_app, name="capture")
|
|
35
|
+
app.add_typer(harness_app, name="harness")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _store_root() -> Path:
|
|
39
|
+
"""Where the harness keeps its store; overridable via JDSL_HARNESS_HOME."""
|
|
40
|
+
return Path(os.environ.get("JDSL_HARNESS_HOME") or (Path.home() / ".local/share/jdsl-harness"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@config_app.command("add")
|
|
44
|
+
def config_add(
|
|
45
|
+
keys: list[str] = typer.Argument(..., help="One or more API keys to store."),
|
|
46
|
+
provider: str = typer.Option("anthropic", "-p", "--provider", help="Provider name."),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Store API keys for a provider (de-duplicated, merged)."""
|
|
49
|
+
try:
|
|
50
|
+
merged = config.add_keys(provider, keys)
|
|
51
|
+
except ValueError as err:
|
|
52
|
+
typer.secho(str(err), fg=typer.colors.RED, err=True)
|
|
53
|
+
raise typer.Exit(1) from None
|
|
54
|
+
typer.secho(
|
|
55
|
+
f"Stored {len(keys)} key(s) for {provider!r} ({len(merged)} total) at {config.auth_path()}.",
|
|
56
|
+
fg=typer.colors.GREEN,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@config_app.command("list")
|
|
61
|
+
def config_list() -> None:
|
|
62
|
+
"""Print the current auth config with keys masked."""
|
|
63
|
+
current = config.load()
|
|
64
|
+
if not current:
|
|
65
|
+
typer.echo("No stored credentials.")
|
|
66
|
+
return
|
|
67
|
+
for provider, entry in current.items():
|
|
68
|
+
typer.secho(provider, fg=typer.colors.CYAN, bold=True)
|
|
69
|
+
for key in entry.get("api_keys", []):
|
|
70
|
+
typer.echo(f" {_mask(key)}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("run")
|
|
74
|
+
def run(
|
|
75
|
+
file: Path = typer.Argument(..., exists=True, readable=True, help="A .py file defining skills."),
|
|
76
|
+
inputs: list[str] = typer.Option(
|
|
77
|
+
[], "-i", "--input", help="Seed a blackboard input as key=value (repeatable)."
|
|
78
|
+
),
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Import ``file`` and execute every ``root()`` skill defined at module level."""
|
|
81
|
+
seed = dict(_parse_kv(item) for item in inputs)
|
|
82
|
+
module = _load_module(file)
|
|
83
|
+
roots = [v for v in vars(module).values() if isinstance(v, Root)]
|
|
84
|
+
if not roots:
|
|
85
|
+
typer.secho(f"No skills (root(...)) found in {file}.", fg=typer.colors.YELLOW)
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
for skill in roots:
|
|
88
|
+
typer.secho(f"▶ running {skill.name!r}", fg=typer.colors.CYAN, bold=True)
|
|
89
|
+
ctx = skill.run(**seed)
|
|
90
|
+
if ctx.blackboard:
|
|
91
|
+
typer.echo(f" blackboard: {dict(ctx.blackboard)}")
|
|
92
|
+
for w in ctx.blackboard.clobbers():
|
|
93
|
+
typer.secho(f" ⚠ {w.key!r} overwritten by {w.writer} (was set by another node)",
|
|
94
|
+
fg=typer.colors.YELLOW)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command("show")
|
|
98
|
+
def show(file: Path = typer.Argument(..., exists=True, readable=True, help="A .py file defining skills.")) -> None:
|
|
99
|
+
"""Print each skill's structure as an ASCII tree (no execution, no model)."""
|
|
100
|
+
module = _load_module(file)
|
|
101
|
+
roots = [v for v in vars(module).values() if isinstance(v, Root)]
|
|
102
|
+
if not roots:
|
|
103
|
+
typer.secho(f"No skills (root(...)) found in {file}.", fg=typer.colors.YELLOW)
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
for skill in roots:
|
|
106
|
+
typer.echo(render(skill))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_kv(item: str) -> tuple[str, str]:
|
|
110
|
+
if "=" not in item: raise typer.BadParameter(f"--input must be key=value, got {item!r}.")
|
|
111
|
+
key, value = item.split("=", 1)
|
|
112
|
+
return key.strip(), value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_module(file: Path):
|
|
116
|
+
spec = importlib.util.spec_from_file_location(file.stem, file)
|
|
117
|
+
if spec is None or spec.loader is None: raise typer.BadParameter(f"Cannot import {file}.")
|
|
118
|
+
module = importlib.util.module_from_spec(spec)
|
|
119
|
+
sys.modules[spec.name] = module
|
|
120
|
+
spec.loader.exec_module(module)
|
|
121
|
+
return module
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _mask(key: str) -> str:
|
|
125
|
+
return "*" * len(key) if len(key) <= 8 else f"{key[:4]}…{key[-4:]}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# -- package commands (§37) ---------------------------------------------------
|
|
129
|
+
|
|
130
|
+
@package_app.command("inspect")
|
|
131
|
+
def package_inspect(path: Path = typer.Argument(..., exists=True, help="A .jdsl or package dir.")) -> None:
|
|
132
|
+
"""Show a package's manifest, capabilities, and burden metrics."""
|
|
133
|
+
from jdsl.package import load_package
|
|
134
|
+
pkg = load_package(path)
|
|
135
|
+
m = pkg.manifest
|
|
136
|
+
typer.secho(f"{m.name} v{m.version} [{m.format}]", fg=typer.colors.CYAN, bold=True)
|
|
137
|
+
typer.echo(f" task family: {m.task_family or 'n/a'}")
|
|
138
|
+
typer.echo(f" verification: {m.verification.get('status', 'unverified')} "
|
|
139
|
+
f"(replay coverage {m.verification.get('replay_coverage', 'n/a')})")
|
|
140
|
+
typer.echo(f" fidelity: {m.source.get('capture_fidelity', 'n/a')}, "
|
|
141
|
+
f"episodes: {m.source.get('episode_count', 'n/a')}")
|
|
142
|
+
perms = pkg.permissions()
|
|
143
|
+
typer.secho(" reads: " + (", ".join(perms["reads"]) or "(none)"), fg=typer.colors.GREEN)
|
|
144
|
+
typer.secho(" writes: " + (", ".join(perms["writes"]) or "(none)"),
|
|
145
|
+
fg=typer.colors.RED if perms["writes"] else typer.colors.GREEN)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@package_app.command("verify")
|
|
149
|
+
def package_verify(path: Path = typer.Argument(..., exists=True, help="A .jdsl or package dir.")) -> None:
|
|
150
|
+
"""Structurally verify a package and its digests; exit non-zero on failure."""
|
|
151
|
+
from jdsl.package import PackageError, load_package
|
|
152
|
+
try:
|
|
153
|
+
load_package(path) # load_package runs digest + structural verification
|
|
154
|
+
except PackageError as err:
|
|
155
|
+
typer.secho(f"✗ invalid package: {err}", fg=typer.colors.RED, err=True)
|
|
156
|
+
raise typer.Exit(1) from None
|
|
157
|
+
typer.secho("✓ package is structurally valid and digests match.", fg=typer.colors.GREEN)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@package_app.command("run")
|
|
161
|
+
def package_run(
|
|
162
|
+
path: Path = typer.Argument(..., exists=True, help="A .jdsl or package dir."),
|
|
163
|
+
tools: Path = typer.Option(..., "-t", "--tools", exists=True,
|
|
164
|
+
help="A .py file exposing TOOLS (dict) and optional PREDICATES."),
|
|
165
|
+
model: str = typer.Option(None, "-m", "--model", help="Model id for residual leaves."),
|
|
166
|
+
inputs: list[str] = typer.Option([], "-i", "--input", help="Seed blackboard key=value."),
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Bind a package to host tools and run it with a (small) model."""
|
|
169
|
+
from jdsl.package import load_package
|
|
170
|
+
pkg = load_package(path)
|
|
171
|
+
mod = _load_module(tools)
|
|
172
|
+
tool_map = getattr(mod, "TOOLS", None)
|
|
173
|
+
if not isinstance(tool_map, dict):
|
|
174
|
+
typer.secho(f"{tools} must define a TOOLS dict (logical_id -> callable).", fg=typer.colors.RED)
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
predicates = getattr(mod, "PREDICATES", {})
|
|
177
|
+
seed = dict(_parse_kv(item) for item in inputs)
|
|
178
|
+
root = pkg.as_root(tool_map, predicates, model_id=model)
|
|
179
|
+
ctx = root.run(**seed)
|
|
180
|
+
typer.echo(f" blackboard: {dict(ctx.blackboard)}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# -- capture commands (§37) ---------------------------------------------------
|
|
184
|
+
|
|
185
|
+
@capture_app.command("start")
|
|
186
|
+
def capture_start(
|
|
187
|
+
host: str = typer.Option("host", help="Source host name."),
|
|
188
|
+
adapter: str = typer.Option("hook", help="Capture adapter name."),
|
|
189
|
+
note: str = typer.Option("", help="Optional capture note."),
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Start a token-protected capture and print hook credentials."""
|
|
192
|
+
from jdsl_harness.capture import CaptureCoordinator
|
|
193
|
+
from jdsl_harness.store import HarnessStore
|
|
194
|
+
session = CaptureCoordinator(HarnessStore(_store_root())).start_session(
|
|
195
|
+
host=host, adapter=adapter, note=note)
|
|
196
|
+
typer.echo(f"capture_id={session.capture_id}")
|
|
197
|
+
typer.echo(f"capture_token={session.token}")
|
|
198
|
+
typer.echo("Export both values to the host hook environment.")
|
|
199
|
+
|
|
200
|
+
@capture_app.command("list")
|
|
201
|
+
def capture_list() -> None:
|
|
202
|
+
"""List captures in the local harness store."""
|
|
203
|
+
from jdsl_harness.store import HarnessStore
|
|
204
|
+
for c in HarnessStore(_store_root()).list_captures():
|
|
205
|
+
typer.echo(f"{c['capture_id']} {c['status']:9} episodes={c['episodes']} {c.get('note','')}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@capture_app.command("import")
|
|
209
|
+
def capture_import(
|
|
210
|
+
file: Path = typer.Argument(..., exists=True, readable=True,
|
|
211
|
+
help="JSONL file: one episode per line {episode_id, steps:[...], outcome}."),
|
|
212
|
+
capture_id: str = typer.Option(..., "-c", "--capture", help="Capture id to import into."),
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Import foreign agent logs into a capture (Tier C, §8.3)."""
|
|
215
|
+
from jdsl_harness.adapters.import_jsonl import import_records_file
|
|
216
|
+
from jdsl_harness.store import HarnessStore
|
|
217
|
+
store = HarnessStore(_store_root())
|
|
218
|
+
events = import_records_file(file, capture_id=capture_id)
|
|
219
|
+
for event in events:
|
|
220
|
+
store.ingest(event)
|
|
221
|
+
typer.secho(f"imported {len(events)} events into {capture_id}.", fg=typer.colors.GREEN)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@capture_app.command("inspect")
|
|
225
|
+
def capture_inspect(capture_id: str = typer.Argument(..., help="Capture id.")) -> None:
|
|
226
|
+
"""Print the exact-lineage report for a capture (§51)."""
|
|
227
|
+
from jdsl_harness.capture import CaptureCoordinator
|
|
228
|
+
from jdsl_harness.store import HarnessStore
|
|
229
|
+
coord = CaptureCoordinator(HarnessStore(_store_root()))
|
|
230
|
+
typer.echo(json.dumps(coord.lineage_report(capture_id), indent=2, default=str))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.command("compile")
|
|
234
|
+
def compile_cmd(
|
|
235
|
+
capture_id: str = typer.Argument(..., help="Capture id to compile."),
|
|
236
|
+
name: str = typer.Option("behavior", "-n", "--name", help="Package name."),
|
|
237
|
+
out: Path = typer.Option(None, "-o", "--out", help="Write a .jdsl to this path."),
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Compile a capture into a verified behavior package (§24)."""
|
|
240
|
+
from jdsl.package import export_jdsl, package_digest
|
|
241
|
+
from jdsl_harness.compiler import compile_behavior
|
|
242
|
+
from jdsl_harness.store import HarnessStore
|
|
243
|
+
store = HarnessStore(_store_root())
|
|
244
|
+
result = compile_behavior(store.capture_episodes(capture_id), name=name)
|
|
245
|
+
typer.echo(json.dumps(result.report(), indent=2, default=str))
|
|
246
|
+
if out is not None:
|
|
247
|
+
path = export_jdsl(result.package, out)
|
|
248
|
+
store.record_package(name, result.package.manifest.version, package_digest(result.package),
|
|
249
|
+
status="built", path=str(path))
|
|
250
|
+
typer.secho(f"→ wrote {path}", fg=typer.colors.GREEN)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# -- harness daemon (§37) -----------------------------------------------------
|
|
254
|
+
|
|
255
|
+
@harness_app.command("serve")
|
|
256
|
+
def harness_serve(
|
|
257
|
+
host: str = typer.Option("127.0.0.1", help="Bind host (loopback by default)."),
|
|
258
|
+
port: int = typer.Option(8848, help="Ingest port."),
|
|
259
|
+
) -> None:
|
|
260
|
+
"""Run the local ingest server (data plane). Ctrl-C to stop."""
|
|
261
|
+
from jdsl_harness.server import IngestServer
|
|
262
|
+
from jdsl_harness.store import HarnessStore
|
|
263
|
+
server = IngestServer(HarnessStore(_store_root()), host=host, port=port).start()
|
|
264
|
+
typer.secho(f"jdsl harness ingest listening on {server.url} (store: {_store_root()})",
|
|
265
|
+
fg=typer.colors.CYAN)
|
|
266
|
+
try:
|
|
267
|
+
import time
|
|
268
|
+
while True:
|
|
269
|
+
time.sleep(3600)
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
server.stop()
|
|
272
|
+
typer.echo("stopped.")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@app.command("tui")
|
|
276
|
+
def tui() -> None:
|
|
277
|
+
"""Open the local skill-authoring workbench."""
|
|
278
|
+
try:
|
|
279
|
+
from jdsl.tui import run
|
|
280
|
+
except ModuleNotFoundError as error:
|
|
281
|
+
if error.name == "textual":
|
|
282
|
+
raise typer.BadParameter("install the TUI with `pip install jdsl[tui]`") from error
|
|
283
|
+
raise
|
|
284
|
+
run()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _print_install_banner():
|
|
288
|
+
green = "\033[0;32m"
|
|
289
|
+
muted = "\033[0;2m"
|
|
290
|
+
nc = "\033[0m"
|
|
291
|
+
typer.echo(f"{green} _ _ _ _ {nc}")
|
|
292
|
+
typer.echo(f"{green} /\\ \\ /\\ \\ / /\\ _\\ \\ {nc}")
|
|
293
|
+
typer.echo(f"{green} \\ \\ \\ / \\ \\____ / / \\ /\\__ \\ {nc}")
|
|
294
|
+
typer.echo(f"{green} /\\ \\_\\ / /\\ \\_____\\ / / /\\ \\__ / /_ \\_\\ {nc}")
|
|
295
|
+
typer.echo(f"{green} / /\\_// / /\\/___ // / /\\ \\___\\ / / /\\/_/ {nc}")
|
|
296
|
+
typer.echo(f"{green} _ / / / / / / / / / \\ \\ \\ \\/___// / / {nc}")
|
|
297
|
+
typer.echo(f"{green} /\\ \\ / / / / / / / / / \\ \\ \\ / / / {nc}")
|
|
298
|
+
typer.echo(f"{green} \\ \\_\\/ / / / / / / / /_ \\ \\ \\ / / / ____ {nc}")
|
|
299
|
+
typer.echo(f"{green} / / /_/ / / \\ \\__/ / //_\\__/ / / / /_/_/ ___/\\ {nc}")
|
|
300
|
+
typer.echo(f"{green} / / /__\\/ / \\ \\___\\/ / \\ \\/___/ / /_______/\\__/ {nc}")
|
|
301
|
+
typer.echo(f"{green}\\/_______/ \\/_____/ \\_____\\/ \\_______\\/ {nc}")
|
|
302
|
+
typer.echo(f"{muted}Quick start:{nc}")
|
|
303
|
+
typer.echo(" jdsl run examples/greeter.py")
|
|
304
|
+
typer.echo("")
|
|
305
|
+
typer.echo(f"{muted}For API keys:{nc}")
|
|
306
|
+
typer.echo(" echo 'ANTHROPIC_API_KEY=sk-...' >> .env")
|
|
307
|
+
typer.echo("")
|
|
308
|
+
typer.echo(f"{muted}Docs: {nc}https://cantor-industries.github.io/jdsl-py/")
|
|
309
|
+
typer.echo("")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@app.callback()
|
|
313
|
+
def main():
|
|
314
|
+
"""Declarative behavior-tree agents.
|
|
315
|
+
|
|
316
|
+
Read docs at https://cantor-industries.github.io/jdsl-py/.
|
|
317
|
+
"""
|
|
318
|
+
# After any subcommand runs, show the banner once on first run or via --about
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command("about")
|
|
323
|
+
def about():
|
|
324
|
+
"""Show jdsl info and quickstart."""
|
|
325
|
+
typer.secho("jdsl — Declarative behavior-tree agents", fg=typer.colors.CYAN, bold=True)
|
|
326
|
+
_print_install_banner()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@app.command("uninstall")
|
|
330
|
+
def uninstall(
|
|
331
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."),
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Remove the curl-installed CLI while preserving captures and credentials."""
|
|
334
|
+
install_root = Path.home() / ".jdsl"
|
|
335
|
+
if not install_root.exists():
|
|
336
|
+
typer.echo("No curl-installed jdsl CLI found.")
|
|
337
|
+
return
|
|
338
|
+
if not yes and not typer.confirm(f"Remove {install_root} and its PATH entry?", default=False):
|
|
339
|
+
typer.echo("Cancelled.")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
shutil.rmtree(install_root)
|
|
343
|
+
removed = _remove_install_path_entries()
|
|
344
|
+
typer.secho("Removed the curl-installed jdsl CLI.", fg=typer.colors.GREEN)
|
|
345
|
+
if removed:
|
|
346
|
+
typer.echo("Removed its PATH entry from: " + ", ".join(removed))
|
|
347
|
+
typer.echo("Harness captures and provider credentials were preserved.")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _remove_install_path_entries() -> list[str]:
|
|
351
|
+
install_dir = Path.home() / ".jdsl" / "bin"
|
|
352
|
+
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
|
|
353
|
+
shell = Path(os.environ.get("SHELL", "sh")).name
|
|
354
|
+
if shell == "fish":
|
|
355
|
+
files = [Path.home() / ".config/fish/config.fish"]
|
|
356
|
+
elif shell == "zsh":
|
|
357
|
+
zdotdir = Path(os.environ.get("ZDOTDIR") or Path.home())
|
|
358
|
+
files = [zdotdir / ".zshrc", zdotdir / ".zshenv", config_home / "zsh/.zshrc", config_home / "zsh/.zshenv"]
|
|
359
|
+
elif shell == "bash":
|
|
360
|
+
files = [Path.home() / ".bashrc", Path.home() / ".bash_profile", Path.home() / ".profile",
|
|
361
|
+
config_home / "bash/.bashrc", config_home / "bash/.bash_profile"]
|
|
362
|
+
else:
|
|
363
|
+
files = [Path.home() / ".bashrc", Path.home() / ".bash_profile", config_home / "bash/.bashrc",
|
|
364
|
+
config_home / "bash/.bash_profile"]
|
|
365
|
+
|
|
366
|
+
removed: list[str] = []
|
|
367
|
+
path_line = f"export PATH={install_dir}:$PATH"
|
|
368
|
+
fish_line = f"fish_add_path {install_dir}"
|
|
369
|
+
for path in dict.fromkeys(files):
|
|
370
|
+
if not path.is_file():
|
|
371
|
+
continue
|
|
372
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
373
|
+
filtered: list[str] = []
|
|
374
|
+
for line in lines:
|
|
375
|
+
if line.strip() in (path_line, fish_line):
|
|
376
|
+
if filtered and filtered[-1].strip() == "# jdsl":
|
|
377
|
+
filtered.pop()
|
|
378
|
+
continue
|
|
379
|
+
filtered.append(line)
|
|
380
|
+
if filtered != lines:
|
|
381
|
+
path.write_text("\n".join(filtered).rstrip() + "\n", encoding="utf-8")
|
|
382
|
+
removed.append(str(path))
|
|
383
|
+
return removed
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__": # pragma: no cover
|
|
387
|
+
app()
|
jdsl/config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Credential storage + provider inference. Keys live in
|
|
2
|
+
~/.local/share/recon/auth.json as {"<provider>": {"api_keys": [...]}}, and a
|
|
3
|
+
.env in the working directory is loaded on import."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
|
|
13
|
+
load_dotenv() # so DEEPSEEK_API_KEY etc. are picked up without exporting
|
|
14
|
+
|
|
15
|
+
SUPPORTED_PROVIDERS = ("anthropic", "openai", "deepseek", "google", "tinker")
|
|
16
|
+
|
|
17
|
+
ENV_KEYS = {
|
|
18
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
19
|
+
"openai": "OPENAI_API_KEY",
|
|
20
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
21
|
+
"google": "GOOGLE_API_KEY",
|
|
22
|
+
"tinker": "TINKER_API_KEY",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# OpenAI-compatible base URLs (Anthropic uses its own SDK, so it's absent).
|
|
26
|
+
BASE_URLS = {
|
|
27
|
+
"deepseek": "https://api.deepseek.com",
|
|
28
|
+
"openai": None,
|
|
29
|
+
"tinker": "https://tinker.thinkingmachines.dev/services/tinker-prod/oai/api/v1",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def provider_for_model(model_id: str) -> str:
|
|
34
|
+
name = model_id.lower()
|
|
35
|
+
if name.startswith("claude"): return "anthropic"
|
|
36
|
+
if name.startswith("deepseek"): return "deepseek"
|
|
37
|
+
if name.startswith(("gpt", "o1", "o3", "o4")): return "openai"
|
|
38
|
+
if name.startswith("gemini"): return "google"
|
|
39
|
+
if name.startswith(("thinkingmachines/", "inkling")): return "tinker"
|
|
40
|
+
return "anthropic"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def config_dir() -> Path:
|
|
44
|
+
base = os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")
|
|
45
|
+
return Path(base) / "recon"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def auth_path() -> Path: return config_dir() / "auth.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load() -> dict[str, dict[str, list[str]]]:
|
|
52
|
+
path = auth_path()
|
|
53
|
+
return json.loads(path.read_text()) if path.exists() else {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def save(config: dict[str, dict[str, list[str]]]) -> None:
|
|
57
|
+
path = auth_path()
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
path.write_text(json.dumps(config, indent=2))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def add_keys(provider: str, keys: list[str]) -> list[str]:
|
|
63
|
+
"""Merge keys into provider's list, de-duplicating. Returns the merged list."""
|
|
64
|
+
if provider not in SUPPORTED_PROVIDERS:
|
|
65
|
+
raise ValueError(f"Unknown provider {provider!r}. Supported: {', '.join(SUPPORTED_PROVIDERS)}.")
|
|
66
|
+
config = load()
|
|
67
|
+
merged = list(config.get(provider, {}).get("api_keys", []))
|
|
68
|
+
for key in keys:
|
|
69
|
+
if key not in merged: merged.append(key)
|
|
70
|
+
config[provider] = {"api_keys": merged}
|
|
71
|
+
save(config)
|
|
72
|
+
return merged
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def keys_for(provider: str) -> list[str]:
|
|
76
|
+
"""Stored keys for provider, else the provider's env var."""
|
|
77
|
+
stored = load().get(provider, {}).get("api_keys", [])
|
|
78
|
+
if stored: return list(stored)
|
|
79
|
+
env_value = os.environ.get(ENV_KEYS.get(provider, ""))
|
|
80
|
+
return [env_value] if env_value else []
|