@vibelord/lanes 1.2.0 → 1.5.0

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 (46) hide show
  1. package/package.json +1 -1
  2. package/payload/lanes/README.md +7 -5
  3. package/payload/lanes/VERSION +1 -1
  4. package/payload/lanes/bin/lanes +1091 -76
  5. package/payload/lanes/container/hooks/grok-cli-hooks.json +8 -0
  6. package/payload/lanes/container/hooks/grok-hooks.json +12 -0
  7. package/payload/lanes/container/hooks/kimi-hooks.toml +49 -0
  8. package/payload/lanes/container/lane +55 -8
  9. package/payload/lanes/container/lane-mcp +120 -22
  10. package/payload/lanes/container/lane-signal +86 -23
  11. package/payload/lanes/container/lane_core.py +524 -36
  12. package/payload/lanes/container/throttle-pulse +456 -0
  13. package/payload/lanes/docs/ARCHITECTURE.md +357 -34
  14. package/payload/lanes/docs/DESIGN.md +13 -3
  15. package/payload/lanes/docs/INSTALL.md +47 -10
  16. package/payload/lanes/docs/RUNBOOK.md +761 -12
  17. package/payload/lanes/docs/USERGUIDE.md +421 -46
  18. package/payload/lanes/holders.lock +5 -0
  19. package/payload/lanes/install/enable-docs.sh +78 -0
  20. package/payload/lanes/install/host-install.sh +39 -5
  21. package/payload/lanes/libexec/agent_state.py +604 -12
  22. package/payload/lanes/libexec/docs_store.py +995 -0
  23. package/payload/lanes/libexec/docsd.py +2010 -0
  24. package/payload/lanes/libexec/economy_poll.py +6 -2
  25. package/payload/lanes/libexec/harness_grok.py +742 -0
  26. package/payload/lanes/libexec/lanes_lib.py +143 -20
  27. package/payload/lanes/libexec/mdlite.py +575 -0
  28. package/payload/lanes/libexec/members.py +5 -0
  29. package/payload/lanes/libexec/notify.py +65 -5
  30. package/payload/lanes/libexec/sidebar.py +626 -25
  31. package/payload/lanes/libexec/statusd.py +530 -32
  32. package/payload/lanes/libexec/tokens.py +42 -9
  33. package/payload/lanes/libexec/update.py +171 -26
  34. package/payload/lanes/libexec/uploads.py +538 -0
  35. package/payload/lanes/libexec/users.py +31 -3
  36. package/payload/lanes/site/site.example.conf +4 -0
  37. package/payload/lanes/systemd/lanes-docsd.service +29 -0
  38. package/payload/lanes/web/Caddyfile +24 -1
  39. package/payload/lanes/web/README.md +86 -8
  40. package/payload/lanes/web/browser.js +2 -4
  41. package/payload/lanes/web/docs.css +52 -0
  42. package/payload/lanes/web/docs.js +36 -0
  43. package/payload/lanes/web/mobile.js +3 -2
  44. package/payload/lanes/web/upload.css +17 -0
  45. package/payload/lanes/web/upload.js +84 -0
  46. package/payload/manifest.json +5 -3
@@ -31,10 +31,17 @@ NEEDS_YOU set. The six-word `state` is never read: the agent status is the only
31
31
  device_active).
32
32
  * Payload: `<lane name>: <agent task> — <status>` and, on a second line, the client command that opens the lane
33
33
  (`lanes-launch lane <id>`). No client registers a URL scheme for it, so the push carries a tappable action only
34
- when `action_url` (a template over `{lane}`) is configured.
34
+ when `action_url` (a template over `{lane}`, and `{doc}` for a `published` event — `action_url()`) is configured.
35
35
  * A failed send is one journal line (lane id and the failure class — never the topic or the bearer) and is not
36
36
  retried; it never stops the service.
37
37
 
38
+ Published documents (plan W10 W4): the docs service's publish pick-up (`lanes-docsd`, docsd.Pickup), not this
39
+ service, calls `notify_published` once a document an agent published from a lane is stored — the same config, the
40
+ same desk-presence rule (device_active) and the same `send`, at most one push per lane per minute (docsd's
41
+ PUBLISHED_PUSH_S), best effort. Its payload is `{title: 'Published', message: '<lane name> published a document (<kind>,
42
+ <size>)\n/lanes-docs/d/<id>', priority: 2}` and, when `action_url` is set, the tap target with `{doc}` resolved —
43
+ never the document's title, name or content: the topic is the host's, and the document may be for one login only.
44
+
38
45
  Enrolment requests (W8 A5, LNS-44): the same service also watches `STATE/enrol-requests/` (lanes-enrold's store;
39
46
  read through `enrold.load_requests`, so a request announced is one `lanes enrol list` shows). A request that is
40
47
  pending — not approved, denied or past `expires_at` — and has not been announced yet yields ONE push:
@@ -55,7 +62,9 @@ Config `~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` overrides the path), `
55
62
  `url` (ntfy server, http/https, required), `topic` (`[A-Za-z0-9_-]{1,64}`, required), `token_file` (a file holding
56
63
  an ntfy access token, optional — sent as a bearer, never logged; an unreadable one disables sending rather than
57
64
  sending unauthenticated), `quiet_when_device_active_s` (default 120; 0 never suppresses), `action_url` (optional
58
- template, e.g. `https://lanes.example/?lane={lane}`). Without a valid `url` and `topic` the service only tracks
65
+ template, e.g. `https://lanes.example/?lane={lane}`; plan W10 W7 adds `{doc}`, the page `/lanes-docs/d/<id>` of the
66
+ document a `published` push names — empty for a needs-you push; the docs service sends those, W4). Without a valid
67
+ `url` and `topic` the service only tracks
59
68
  transitions and sends nothing. The config is re-read when notify.conf OR its `token_file` changes (inode, size,
60
69
  mtime or ctime, so a token written, rotated, removed or chmod-ed after notify.conf counts), and on every tick while
61
70
  sending is disabled, so a token that becomes readable switches the pushes on within one interval.
@@ -76,6 +85,7 @@ import urllib.request
76
85
  from pathlib import Path
77
86
 
78
87
  sys.path.insert(0, str(Path(__file__).resolve().parent))
88
+ import docs_store # noqa: E402
79
89
  import enrold as E # noqa: E402
80
90
  import lanes_lib as L # noqa: E402
81
91
  import tokens # noqa: E402
@@ -95,6 +105,8 @@ PRESENCE = '*.pid' # clients/ files only
95
105
  SEEN = '.seen.json' # a sidebar's opened-lane record; desk devices only
96
106
  # (a .pid of any age, devices/<device>.json not mobile)
97
107
  # users/<login>.json written by a desk device counts too
108
+ PUBLISHED_TITLE = 'Published' # a document published from a lane (notify_published)
109
+ PUBLISHED_PRIORITY = 2
98
110
  ENROL_DIR = 'enrol-requests' # STATE/<this>/<id>.json: lanes-enrold's requests
99
111
  ENROL_SEEN = 'notify.enrol-seen.json' # STATE/<this>: the request ids already announced
100
112
  ENROL_TITLE = 'Enrol request'
@@ -275,6 +287,18 @@ def device_active(clients_dir, window, now, devices_dir=None, users_dir=None, me
275
287
  return any(within(at, window) for _login, at in marks)
276
288
 
277
289
 
290
+ def action_url(template, lane_id, doc_id=None):
291
+ """notify.conf's `action_url` template made a push's tap target -> the URL, or '' without a template. `{lane}` is
292
+ the lane id; `{doc}` (plan W10 W7) the page of the document a `published` event is about, `/lanes-docs/d/<id>`
293
+ (docs_store.URL; a docs_store id, else nothing), and nothing for an event without one (a needs-you push) — so
294
+ `https://<host>:8443{doc}` opens the document. Both are percent-quoted."""
295
+ if not template:
296
+ return ''
297
+ doc = docs_store.URL.format(doc_id) if isinstance(doc_id, str) and docs_store.ID_RE.match(doc_id) else ''
298
+ return (template.replace('{lane}', urllib.parse.quote(lane_id, safe=''))
299
+ .replace('{doc}', urllib.parse.quote(doc, safe='/')))
300
+
301
+
278
302
  def _note(lane_id, status, agent, names, config):
279
303
  label = LABELS.get(status, status)
280
304
  name = _text(names.get(lane_id) or lane_id)
@@ -282,14 +306,50 @@ def _note(lane_id, status, agent, names, config):
282
306
  first = _line(f'{name}: {task} — {label}' if task else f'{name} — {label}')
283
307
  note = {'lane': lane_id, 'status': status, 'title': label,
284
308
  'message': f'{first}\nlanes-launch lane {lane_id}', 'priority': PRIORITY.get(status, 3)}
285
- template = config.get('action_url') or ''
286
- if template:
287
- url = template.replace('{lane}', urllib.parse.quote(lane_id, safe=''))
309
+ url = action_url(config.get('action_url') or '', lane_id)
310
+ if url:
288
311
  note['click'] = url
289
312
  note['actions'] = [{'action': 'view', 'label': 'Open lane', 'url': url}]
290
313
  return note
291
314
 
292
315
 
316
+ def _size(size):
317
+ if size < 1024:
318
+ return f'{size} B'
319
+ return f'{size / 1024:.1f} KiB' if size < 1024 * 1024 else f'{size / (1024 * 1024):.1f} MiB'
320
+
321
+
322
+ def published_note(lane_id, meta, names, config):
323
+ """The push for a document stored from lane `lane_id` (`meta` its docs_store meta): the lane's name, the kind and
324
+ the size, and the document's page (module docstring) — never its title, name or content."""
325
+ name = _text(names.get(lane_id) or lane_id)
326
+ first = _line(f"{name} published a document ({_text(meta.get('kind'))}, {_size(int(meta.get('bytes') or 0))})")
327
+ note = {'lane': lane_id, 'status': 'published', 'doc': meta['id'], 'title': PUBLISHED_TITLE,
328
+ 'message': f"{first}\n{docs_store.URL.format(meta['id'])}", 'priority': PUBLISHED_PRIORITY}
329
+ url = action_url(config.get('action_url') or '', lane_id, meta['id'])
330
+ if url:
331
+ note['click'] = url
332
+ note['actions'] = [{'action': 'view', 'label': 'Open document', 'url': url}]
333
+ return note
334
+
335
+
336
+ def notify_published(lane_id, meta, names=None, conf_path=None, state=None, now=None, opener=None):
337
+ """Send the `published` push for a stored document (the docs service's pick-up calls it, W4) -> the note sent, or
338
+ None: sending disabled (no valid notify.conf at `conf_path`, else $LANES_NOTIFY_CONF, else the default), somebody
339
+ at a desk (device_active over `state`, default L.STATE), or the send failed (one journal line). Never raises for a
340
+ failed send; the caller rate-limits."""
341
+ config = read_config(conf_path or os.environ.get('LANES_NOTIFY_CONF') or DEFAULT_CONF)
342
+ if not config['enabled']:
343
+ return None
344
+ state = Path(state) if state is not None else L.STATE
345
+ now = time.time() if now is None else now
346
+ window = int(config.get('quiet_when_device_active_s', DEFAULT_QUIET) or 0)
347
+ if device_active(state / 'clients', window, now, state / 'devices'):
348
+ return None
349
+ note = published_note(lane_id, meta, names or {}, config)
350
+ return note if send(note, config, opener) else None
351
+
352
+
293
353
  def notify_changes(prev_status, new_status, config, names=None, now=None, clients_dir=None, devices_dir=None):
294
354
  """The notifications for lanes whose agent status ENTERED needs-you between two status.json documents.
295
355
  `prev_status` None = the first read: seed only, nothing is news. Empty when a desk device is present."""