kijito-tools 0.2.3 → 0.2.4
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.
- package/package.json +1 -1
- package/providers/monitor/CHANGELOG.md +13 -0
- package/providers/monitor/README.md +1 -1
- package/providers/monitor/UPSTREAM +10 -10
- package/providers/monitor/com.kijito.inbox-monitor.plist.template +2 -2
- package/providers/monitor/kijito_inbox_monitor.py +12 -4
- package/providers/monitor/package.json +1 -1
- package/providers/monitor/pyproject.toml +1 -1
- package/providers/monitor/test_kijito_monitor.py +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kijito-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Installer for kijito-tools: copies the context-tracking, session catch-up, and self-clear scripts plus the Kijito skills into ~/.claude.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to kijito-inbox-monitor are documented in this file.
|
|
4
4
|
The format is based on Keep a Changelog, and this project follows Semantic Versioning.
|
|
5
5
|
|
|
6
|
+
## [0.5.6] - 2026-09-25
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **The launchd template names the state file the producer actually writes (row M289).** It passed a base
|
|
10
|
+
(`--state-file <dir>/hive.json`) that the producer turned into `hive.<persona>.json`, so anyone reading the
|
|
11
|
+
template looked for a file that never existed. It now passes `--state-file-template <dir>/hive.{persona}.json`,
|
|
12
|
+
which resolves to the SAME file as before, so upgrading moves nothing. The README's multi-persona example
|
|
13
|
+
likewise names `state.{persona}.json`. A new `_state_path_from_args()` is the one place the producer
|
|
14
|
+
decides a persona's state file, and `StateFileNameAgreementTest` derives the name from the template's own
|
|
15
|
+
argv through it.
|
|
16
|
+
- **A negative persisted `consecutive_failures` makes the state file CORRUPT.** A count below zero was
|
|
17
|
+
resumed as-is and silently postponed the dead-man alert by that many polls.
|
|
18
|
+
|
|
6
19
|
## [0.5.5] - 2026-09-24
|
|
7
20
|
|
|
8
21
|
### Added
|
|
@@ -269,7 +269,7 @@ owned, rotated event file, and each agent session consumes only its own:
|
|
|
269
269
|
```sh
|
|
270
270
|
kijito-inbox-monitor --all-personas \
|
|
271
271
|
--events-file-template ~/.local/state/kijito-inbox-monitor/events.{persona}.ndjson \
|
|
272
|
-
--state-file ~/.local/state/kijito-inbox-monitor/state.json
|
|
272
|
+
--state-file-template ~/.local/state/kijito-inbox-monitor/state.{persona}.json
|
|
273
273
|
```
|
|
274
274
|
|
|
275
275
|
Each session then wakes on its own `events.<persona>.ndjson` using the recipe in
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
# The upstream release providers/monitor is a byte-exact copy of (row M314).
|
|
2
2
|
# Written by scripts/import-monitor.sh; verified by tests/vendored_monitor_test.sh. Never hand-edit.
|
|
3
3
|
repo=KijitoAI/kijito-inbox-monitor
|
|
4
|
-
sha=
|
|
5
|
-
tree=
|
|
6
|
-
tag=v0.5.
|
|
4
|
+
sha=04bb12ae11fc882d14c30ac36a9220591b5fab65
|
|
5
|
+
tree=101419def29ffc023991a6f5aace5933d042768a
|
|
6
|
+
tag=v0.5.6
|
|
7
7
|
file=100644 8d88ccb0fd09b9144505f985b4f882db278fac1f .github/workflows/publish-npm.yml
|
|
8
8
|
file=100644 19ce3471a2cffa689be9d261049c1e74e3e7a25e .github/workflows/publish-pypi.yml
|
|
9
9
|
file=100644 4534290a5dd36d18d07a782b784b6b0ad5ee38fc .gitignore
|
|
10
|
-
file=100644
|
|
10
|
+
file=100644 8d92a7f3d76e6802faed79328e349b6b7786b87e CHANGELOG.md
|
|
11
11
|
file=100644 d645695673349e3947e8e5ae42332d0ac3164cd7 LICENSE
|
|
12
12
|
file=100644 f6b0e5634f3fd4156c1c000efb31578bfb068cf3 NOTICE
|
|
13
|
-
file=100644
|
|
13
|
+
file=100644 73d2bf5a5d5d6cb622db014285cd9ada4c705724 README.md
|
|
14
14
|
file=100644 72f0c148f1e721ac1bb4a38e5cae690355774314 RELEASING.md
|
|
15
15
|
file=100755 fca1203267cb726736b0c8f239e2d74c40a7a895 arm-hive-monitor.sh
|
|
16
16
|
file=100755 bbe3f07de13dab8a8a7a4798481156ae4a46ab8f bin/cli.js
|
|
17
|
-
file=100644
|
|
17
|
+
file=100644 fffa8bd23749a321285bd942a3f4f4a98936129c com.kijito.inbox-monitor.plist.template
|
|
18
18
|
file=100644 30519b1ad6098541083648392f344da68e688246 docs/DESIGN.md
|
|
19
19
|
file=100644 4f5e53a8633e60e6c57c451e28546f8af8783fe8 kijito-inbox-monitor@.service.template
|
|
20
|
-
file=100644
|
|
21
|
-
file=100644
|
|
22
|
-
file=100644
|
|
20
|
+
file=100644 5038ff752d4b936e5b9bfab5642f38e7622fd4a4 kijito_inbox_monitor.py
|
|
21
|
+
file=100644 5e682088a3dafbe9526761c87f9a16573c3bbdc9 package.json
|
|
22
|
+
file=100644 6f90a103962a6ee2fa985f8356ff8c95cb93a6a7 pyproject.toml
|
|
23
23
|
file=100755 b34a5c775385ecfaefd4b374c1fc1aa082a85d14 scripts/migrate-systemd-unit.sh
|
|
24
24
|
file=100755 22acef6d6875395a81c701eb535030d0a78b0df1 scripts/mutation-check.py
|
|
25
25
|
file=100755 acba6196213736922fc4f7135749386d85d172a6 scripts/prepublish-gate.sh
|
|
26
|
-
file=100644
|
|
26
|
+
file=100644 ea22077c22b25d53d6719af4b0e7d9e719aeea7d test_kijito_monitor.py
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
<string>__PROGRAM__</string>
|
|
38
38
|
<string>--token-file</string>
|
|
39
39
|
<string>__HOME__/.config/kijito-inbox-monitor/token</string>
|
|
40
|
-
<string>--state-file</string>
|
|
41
|
-
<string>__HOME__/.cache/kijito-inbox-monitor/hive.json</string>
|
|
40
|
+
<string>--state-file-template</string>
|
|
41
|
+
<string>__HOME__/.cache/kijito-inbox-monitor/hive.{persona}.json</string>
|
|
42
42
|
<string>--events-file-template</string>
|
|
43
43
|
<string>__HOME__/.cache/kijito-inbox-monitor/events.{persona}.ndjson</string>
|
|
44
44
|
<string>--heartbeat</string>
|
|
@@ -33,7 +33,7 @@ try:
|
|
|
33
33
|
except ImportError: # pragma: no cover - Windows
|
|
34
34
|
fcntl = None
|
|
35
35
|
|
|
36
|
-
__version__ = "0.5.
|
|
36
|
+
__version__ = "0.5.6"
|
|
37
37
|
SOURCE = "kijito-inbox"
|
|
38
38
|
# A named User-Agent is REQUIRED: api.kijito.ai is fronted by a WAF that 403s the default Python-urllib UA.
|
|
39
39
|
USER_AGENT = "kijito-inbox-monitor/%s" % __version__
|
|
@@ -1543,7 +1543,7 @@ class StateFile:
|
|
|
1543
1543
|
"%s\n" % self.path)
|
|
1544
1544
|
return CORRUPT_STATE
|
|
1545
1545
|
if not ((cursor is None or _is_int(cursor)) and state in ("UP", "DOWN")
|
|
1546
|
-
and _is_int(failures)):
|
|
1546
|
+
and _is_int(failures) and failures >= 0): # a failure COUNT is never negative
|
|
1547
1547
|
sys.stderr.write("kijito-inbox-monitor: WARNING state-file has a valid envelope but invalid "
|
|
1548
1548
|
"fields; refusing to baseline over it: %s\n" % self.path)
|
|
1549
1549
|
return CORRUPT_STATE
|
|
@@ -1858,6 +1858,15 @@ def _persona_path(template, persona):
|
|
|
1858
1858
|
return template.replace("{persona}", _state_safe_persona(persona))
|
|
1859
1859
|
|
|
1860
1860
|
|
|
1861
|
+
def _state_path_from_args(args, persona):
|
|
1862
|
+
"""THE state file a persona's watch target opens (row M289): the `{persona}` template when given, else the
|
|
1863
|
+
per-persona derivation of the `--state-file` base. One function, so a supervisor template, the docs and a
|
|
1864
|
+
test can ask the producer which file it will write instead of re-deriving the name and drifting."""
|
|
1865
|
+
if getattr(args, "state_file_template", None):
|
|
1866
|
+
return _persona_path(args.state_file_template, persona)
|
|
1867
|
+
return _state_path_for_persona(getattr(args, "state_file", None), persona)
|
|
1868
|
+
|
|
1869
|
+
|
|
1861
1870
|
def _created_epoch(created):
|
|
1862
1871
|
"""A message's `created` as epoch seconds, or None if it cannot be read. The server writes naive UTC
|
|
1863
1872
|
('2026-09-24 21:34:24.943580'); an explicit offset or 'Z' is honoured when present."""
|
|
@@ -2022,8 +2031,7 @@ class WatchTarget:
|
|
|
2022
2031
|
cp = urllib.parse.urlsplit(url)
|
|
2023
2032
|
self.unread_persona = dict(urllib.parse.parse_qsl(cp.query)).get("persona") or persona
|
|
2024
2033
|
|
|
2025
|
-
state_path = (
|
|
2026
|
-
else _state_path_for_persona(args.state_file, persona))
|
|
2034
|
+
state_path = _state_path_from_args(args, persona)
|
|
2027
2035
|
if state_path:
|
|
2028
2036
|
self.state_file = StateFile(state_path, self.identity)
|
|
2029
2037
|
if not args.self_test:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kijito-inbox-monitor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Pointer to the Python package kijito-inbox-monitor. A zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives. Install the real tool with pipx/uv/pip.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "kijito-inbox-monitor"
|
|
10
|
-
version = "0.5.
|
|
10
|
+
version = "0.5.6"
|
|
11
11
|
description = "Zero-dependency watcher that wakes a running agent when new Kijito inbox mail arrives."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
@@ -6114,3 +6114,82 @@ class StillUnreadBackstopTest(unittest.TestCase):
|
|
|
6114
6114
|
self.assertIn(">= 0", err(["--persona", "a", "--still-unread-after", "-1"]))
|
|
6115
6115
|
self.assertIn(">= 1", err(["--persona", "a", "--still-unread-max", "0"]))
|
|
6116
6116
|
self.assertIsNone(err(["--persona", "a", "--still-unread-after", "0"]))
|
|
6117
|
+
|
|
6118
|
+
|
|
6119
|
+
class StateFileNameAgreementTest(unittest.TestCase):
|
|
6120
|
+
"""Row M289 (second half): the launchd template, the docs and the producer must agree on the state file's
|
|
6121
|
+
NAME. The template used to pass a base (`--state-file <dir>/hive.json`) that the producer silently turned
|
|
6122
|
+
into `hive.<persona>.json`, so a reader of the template looked for a file that never existed. Now the
|
|
6123
|
+
template names the file with a `{persona}` template and this test derives the name from the template's
|
|
6124
|
+
OWN argv, through the producer's OWN parser and `_state_path_from_args` - one constant, no second copy."""
|
|
6125
|
+
|
|
6126
|
+
NAMES = ["argus", "Loom", "name (purpose)", "Ωmega", "two words", "Claude-chat"]
|
|
6127
|
+
HOME = "/HOMEDIR"
|
|
6128
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
6129
|
+
|
|
6130
|
+
def _launchd_argv(self):
|
|
6131
|
+
with open(os.path.join(self._HERE, "com.kijito.inbox-monitor.plist.template"), encoding="utf-8") as fh:
|
|
6132
|
+
text = fh.read()
|
|
6133
|
+
block = re.search(r"<key>ProgramArguments</key>\s*<array>(.*?)</array>", text, re.S).group(1)
|
|
6134
|
+
words = [w.replace("__HOME__", self.HOME) for w in re.findall(r"<string>([^<]*)</string>", block)]
|
|
6135
|
+
return words[words.index("__PROGRAM__") + 1:]
|
|
6136
|
+
|
|
6137
|
+
def _args(self):
|
|
6138
|
+
args = km.build_parser().parse_args(self._launchd_argv())
|
|
6139
|
+
km.validate_args(args)
|
|
6140
|
+
return args
|
|
6141
|
+
|
|
6142
|
+
def test_the_launchd_template_names_exactly_the_state_file_the_producer_writes(self):
|
|
6143
|
+
args = self._args()
|
|
6144
|
+
self.assertIsNotNone(args.state_file_template, "the template must NAME the per-persona file, not a base")
|
|
6145
|
+
for name in self.NAMES:
|
|
6146
|
+
with self.subTest(persona=name):
|
|
6147
|
+
path = km._state_path_from_args(args, name)
|
|
6148
|
+
self.assertEqual(os.path.basename(path), "hive.%s.json" % km._state_safe_persona(name))
|
|
6149
|
+
|
|
6150
|
+
def test_existing_launchd_installs_keep_their_state_file(self):
|
|
6151
|
+
# The old template's base `hive.json` was derived to `hive.<safe>.json`; the new template must land on
|
|
6152
|
+
# the SAME file, or an upgrade would baseline over every persona's cursor (skipping its backlog).
|
|
6153
|
+
args = self._args()
|
|
6154
|
+
legacy = self.HOME + "/.cache/kijito-inbox-monitor/hive.json"
|
|
6155
|
+
for name in self.NAMES:
|
|
6156
|
+
with self.subTest(persona=name):
|
|
6157
|
+
self.assertEqual(km._state_path_from_args(args, name), km._state_path_for_persona(legacy, name))
|
|
6158
|
+
|
|
6159
|
+
def test_the_producer_opens_the_path_state_path_from_args_returns(self):
|
|
6160
|
+
# Pins that the watch target uses THIS function, so the test above speaks for the running producer.
|
|
6161
|
+
with open(os.path.join(self._HERE, "kijito_inbox_monitor.py"), encoding="utf-8") as fh:
|
|
6162
|
+
src = fh.read()
|
|
6163
|
+
self.assertIn("state_path = _state_path_from_args(args, persona)", src)
|
|
6164
|
+
|
|
6165
|
+
def test_the_readme_multi_persona_example_writes_the_file_it_documents(self):
|
|
6166
|
+
with open(os.path.join(self._HERE, "README.md"), encoding="utf-8") as fh:
|
|
6167
|
+
readme = fh.read()
|
|
6168
|
+
m = re.search(r"--all-personas \\\n(?:.*\\\n)*?\s*--state-file-template (\S+)", readme)
|
|
6169
|
+
self.assertIsNotNone(m, "the multi-persona example must name its state file with a {persona} template")
|
|
6170
|
+
args = km.build_parser().parse_args(["--all-personas", "--state-file-template", m.group(1)])
|
|
6171
|
+
self.assertEqual(os.path.basename(km._state_path_from_args(args, "argus")), "state.argus.json")
|
|
6172
|
+
self.assertIn("state.<persona>.json", readme)
|
|
6173
|
+
|
|
6174
|
+
|
|
6175
|
+
class NegativeFailureCountStateTest(unittest.TestCase):
|
|
6176
|
+
"""A persisted `consecutive_failures` below zero is not a count: resumed as-is it would postpone the
|
|
6177
|
+
dead-man edge by that many polls, silently. `_is_int` alone accepted it (a negative is a real integer)."""
|
|
6178
|
+
|
|
6179
|
+
def _load(self, failures):
|
|
6180
|
+
with tempfile.TemporaryDirectory() as d:
|
|
6181
|
+
p = os.path.join(d, "s.json")
|
|
6182
|
+
with open(p, "w") as f:
|
|
6183
|
+
json.dump({"identity": "idx", "cursor": 100, "state": "UP", "consecutive_failures": failures}, f)
|
|
6184
|
+
err, sys.stderr = sys.stderr, io.StringIO()
|
|
6185
|
+
try:
|
|
6186
|
+
return km.StateFile(p, "idx").load()
|
|
6187
|
+
finally:
|
|
6188
|
+
sys.stderr = err
|
|
6189
|
+
|
|
6190
|
+
def test_a_negative_count_is_CORRUPT(self):
|
|
6191
|
+
for bad in (-1, -1000):
|
|
6192
|
+
self.assertIs(self._load(bad), km.CORRUPT_STATE, "consecutive_failures=%r must not be resumed" % bad)
|
|
6193
|
+
|
|
6194
|
+
def test_zero_still_loads(self):
|
|
6195
|
+
self.assertIsNot(self._load(0), km.CORRUPT_STATE)
|