ws-aitoolkit 0.1.2__tar.gz → 0.1.4__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.
Files changed (89) hide show
  1. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/PKG-INFO +1 -1
  2. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/pyproject.toml +1 -1
  3. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/pyproject.toml.orig +1 -1
  4. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/_build.py +2 -2
  5. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/extcmd.py +5 -2
  6. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/syntax_check.py +17 -1
  7. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/policy.py +25 -4
  8. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/LICENSE +0 -0
  9. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/NOTICE +0 -0
  10. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/README.md +0 -0
  11. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/__init__.py +0 -0
  12. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/__main__.py +0 -0
  13. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/adapters/__init__.py +0 -0
  14. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/adapters/claude_code.py +0 -0
  15. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/adapters/opencode.py +0 -0
  16. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/adapters/opencode_plugin.py +0 -0
  17. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/approvals.py +0 -0
  18. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/broker.py +0 -0
  19. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/canary.py +0 -0
  20. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/cli.py +0 -0
  21. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/config_cmd.py +0 -0
  22. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/devices.py +0 -0
  23. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/doctor.py +0 -0
  24. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/eval.py +0 -0
  25. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/evalstats.py +0 -0
  26. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/frontmatter.py +0 -0
  27. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/gate.py +0 -0
  28. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/globals.py +0 -0
  29. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/__init__.py +0 -0
  30. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/_paths.py +0 -0
  31. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/bulk_staging.py +0 -0
  32. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/claims.py +0 -0
  33. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/record_calls.py +0 -0
  34. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/session_context.py +0 -0
  35. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/skill_gate.py +0 -0
  36. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/spawn_cap.py +0 -0
  37. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/guards/test_weakening.py +0 -0
  38. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/install.py +0 -0
  39. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/jobs.py +0 -0
  40. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/jsonl.py +0 -0
  41. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/mcp_server.py +0 -0
  42. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/memory.py +0 -0
  43. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/modules.py +0 -0
  44. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/notes.py +0 -0
  45. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/notesdetect.py +0 -0
  46. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/osinfo.py +0 -0
  47. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/output.py +0 -0
  48. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/profile.py +0 -0
  49. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/rating.py +0 -0
  50. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/__init__.py +0 -0
  51. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/_schedule.py +0 -0
  52. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/anthropic.py +0 -0
  53. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/commands.py +0 -0
  54. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/launchd.py +0 -0
  55. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/markdown_git.py +0 -0
  56. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/notify_none.py +0 -0
  57. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/ntfy.py +0 -0
  58. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/opencode.py +0 -0
  59. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/restic.py +0 -0
  60. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/secrets_env.py +0 -0
  61. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/secrets_ws.py +0 -0
  62. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/sops_age.py +0 -0
  63. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/roles/systemd.py +0 -0
  64. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/run.py +0 -0
  65. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/runrecord.py +0 -0
  66. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/runschema.py +0 -0
  67. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/sandbox.py +0 -0
  68. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/secrets.py +0 -0
  69. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/selfcmd.py +0 -0
  70. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/session.py +0 -0
  71. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/setup_ui/__init__.py +0 -0
  72. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/setup_ui/brand.py +0 -0
  73. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/setup_ui/flow.py +0 -0
  74. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/setup_ui/inline.py +0 -0
  75. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/setup_ui/web.py +0 -0
  76. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/__init__.py +0 -0
  77. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/align.py +0 -0
  78. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/checkpoint.py +0 -0
  79. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/dataviz.py +0 -0
  80. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/resume.py +0 -0
  81. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/ship.py +0 -0
  82. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/skills/taste_gate.py +0 -0
  83. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/status.py +0 -0
  84. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/testing/__init__.py +0 -0
  85. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/testing/mockprovider.py +0 -0
  86. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/testing/scenarios.py +0 -0
  87. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/usage.py +0 -0
  88. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/userconfig.py +0 -0
  89. {ws_aitoolkit-0.1.2 → ws_aitoolkit-0.1.4}/src/ws/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ws-aitoolkit
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Your memory, skills and rules, in any AI.
5
5
  Author: Michael Fry
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ws-aitoolkit"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Your memory, skills and rules, in any AI."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ws-aitoolkit"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "Your memory, skills and rules, in any AI."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  """Written by the release workflow before uv build; absent in a checkout."""
2
2
 
3
- git_sha = "92389d65eb2553fe9d570d9fd6f37cb386b08c01"
3
+ git_sha = "9ed7a1d78df76dd7e28498dd93f03890e6b1efb1"
4
4
  dirty = False
5
- build_date = "2026-11-01T07:40:02Z"
5
+ build_date = "2026-11-01T15:26:17Z"
6
6
  source = "release"
@@ -2,7 +2,7 @@
2
2
 
3
3
  Precedence, first match wins: a built-in command; `ws-<word>` on PATH (a word is `[a-z][a-z0-9-]{0,40}`); the
4
4
  profile's `[cli] fallback`; the usual usage error with "did you mean". An external gets the arguments after its word,
5
- `WS_OUTPUT=json` when `--json` was given, and `-C PATH` as its working folder (the way git runs `git-<word>`). The
5
+ `--json` (before any `--`) and `WS_OUTPUT=json` when `--json` was given, and `-C PATH` as its working folder (the way git runs `git-<word>`). The
6
6
  fallback gets the whole original argument list, leading flags and the empty list (a bare `ws`) included, and runs with
7
7
  `WS_FALLBACK=1`; a run that already has it set is not handed on again. The fallback is an absolute path (or `~/...`) to a
8
8
  file that is yours, executable and closed to other writers, the same test a job file passes, in a folder that other
@@ -177,9 +177,12 @@ def plan(
177
177
  found = find_external(first, env)
178
178
  if found:
179
179
  child = dict(env)
180
+ args = rest[1:]
180
181
  if g.json:
181
182
  child["WS_OUTPUT"] = "json"
182
- return Launch(found, rest[1:], child)
183
+ cut = args.index("--") if "--" in args else len(args)
184
+ args = [*args[:cut], "--json", *args[cut:]] # passed on as well: most programs read the flag
185
+ return Launch(found, args, child)
183
186
  elif first is not None and first.split("=")[0] in flags:
184
187
  return None
185
188
  try:
@@ -104,6 +104,22 @@ def _run(argv: list[str], path: str) -> tuple[str | None, str | None]:
104
104
  return f"{path}: {argv[0]} found a problem:\n{_brief(done.stdout + done.stderr)}", None
105
105
 
106
106
 
107
+ def _bash_shebang(path: str) -> bool:
108
+ """Does the script ask for bash on its first line (`#!/bin/bash`, `#!/usr/bin/env bash`)? Then `sh -n` would
109
+ report bash syntax such as `>(...)` as an error."""
110
+ try:
111
+ with open(path, "rb") as f:
112
+ first = f.readline(256)
113
+ except OSError:
114
+ return False
115
+ if not first.startswith(b"#!"):
116
+ return False
117
+ words = first[2:].split()
118
+ if words and words[0].endswith(b"/env"):
119
+ words = [w for w in words[1:] if not w.startswith(b"-")]
120
+ return bool(words) and words[0].rsplit(b"/", 1)[-1] == b"bash"
121
+
122
+
107
123
  def judge(call, cfg: Config, ctx) -> Finding | None:
108
124
  path = os.path.abspath(os.path.join(ctx.cwd or "", call.value))
109
125
  ext = path.rsplit(".", 1)[-1].lower() if "." in os.path.basename(path) else ""
@@ -118,7 +134,7 @@ def judge(call, cfg: Config, ctx) -> Finding | None:
118
134
  data = f.read()
119
135
  problem, note = {"py": _python, "json": _json, "toml": _toml}[ext](path, data), None
120
136
  elif ext in ("sh", "bash"):
121
- shell = "bash" if ext == "bash" else "sh"
137
+ shell = "bash" if ext == "bash" or _bash_shebang(path) else "sh"
122
138
  if shutil.which(shell) is None and shutil.which("bash") is not None:
123
139
  shell = "bash"
124
140
  problem, note = _run([shell, "-n"], path)
@@ -1131,6 +1131,28 @@ def _cluster(flags: list[str], letter: str) -> bool:
1131
1131
  # --- paths: deny_read, protected files ----------------------------------------------------------------------------
1132
1132
 
1133
1133
 
1134
+ def _glob_class(pattern: str, i: int) -> tuple[str, int] | None:
1135
+ """The bracket class opening at `pattern[i]` as a regex class and the index of its `]`, or None when it is not
1136
+ one (no `]` before a `/`, or a range like `s-c` that runs backwards), so the `[` is matched as itself."""
1137
+ j = pattern.find("]", i + 2)
1138
+ if j < 0 or "/" in pattern[i + 1 : j]:
1139
+ return None
1140
+ body = pattern[i + 1 : j]
1141
+ neg = body[0] in "!^"
1142
+ body = body[1:] if neg else body
1143
+ parts, k = [], 0
1144
+ while k < len(body):
1145
+ if k + 2 < len(body) and body[k + 1] == "-":
1146
+ if body[k] > body[k + 2]:
1147
+ return None
1148
+ parts.append(re.escape(body[k]) + "-" + re.escape(body[k + 2]))
1149
+ k += 3
1150
+ else:
1151
+ parts.append(re.escape(body[k]))
1152
+ k += 1
1153
+ return "[" + ("^" if neg else "") + "".join(parts) + "]", j
1154
+
1155
+
1134
1156
  def _glob_regex_path(pattern: str) -> re.Pattern:
1135
1157
  out, i = [], 0
1136
1158
  while i < len(pattern):
@@ -1147,10 +1169,9 @@ def _glob_regex_path(pattern: str) -> re.Pattern:
1147
1169
  out.append("[^/]*")
1148
1170
  elif c == "?":
1149
1171
  out.append("[^/]")
1150
- elif c == "[" and (j := pattern.find("]", i + 2)) > 0:
1151
- body = pattern[i + 1 : j]
1152
- out.append("[" + ("^" + body[1:] if body[0] == "!" else body) + "]")
1153
- i = j
1172
+ elif c == "[" and (cls := _glob_class(pattern, i)) is not None:
1173
+ out.append(cls[0])
1174
+ i = cls[1]
1154
1175
  else:
1155
1176
  out.append(re.escape(c))
1156
1177
  i += 1
File without changes
File without changes
File without changes
File without changes
File without changes