shraga 0.1.73 → 0.1.74

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/cli.ts CHANGED
@@ -29,6 +29,9 @@ Subcommands:
29
29
  ingress Run the host-header TCP router (INGRESS_PORT, default 3100)
30
30
  for previews + blue-green flips. Own process, survives restarts.
31
31
  user add <email> <pw> Seed a local username/password user
32
+ para post [text] Push a message into a linked para.li conversation.
33
+ Text from [text], --file <path>, or stdin (preferred for
34
+ multi-line reports). --conn <id> picks a link when several exist.
32
35
 
33
36
  Environment:
34
37
  CLOUDFLARE_TUNNEL_TOKEN If set, starts a Cloudflare Tunnel alongside the server.
@@ -59,6 +62,60 @@ if (args[0] === 'user' && args[1] === 'add') {
59
62
  process.exit(0);
60
63
  }
61
64
 
65
+ // `shraga para post [text]` — the PROACTIVE half of the para.li agent lane.
66
+ //
67
+ // The reactive half (para asks, shraga answers) is driven by para.li calling `/api/para/turn`, and
68
+ // the only other outbound path is the deploy-notice bus subscriber in `para/feature.ts` — which is
69
+ // gated on `kind === 'deploy'`. So a SCHEDULED run (a daily digest) had no way to reach the lane at
70
+ // all: it composes text on its own clock with no inbound turn to answer and no deploy event to ride.
71
+ // This is that door, and it is a CLI rather than a route because the caller is the agent itself,
72
+ // running Bash on this very box: a local process reading the same `para-links.json` the feature
73
+ // writes needs no listener, no API key, and no second copy of the callback secret.
74
+ //
75
+ // Text comes from stdin by default. A digest is multi-line markdown with backticks and emoji, and
76
+ // making a model shell-quote that into argv is a defect generator; `... | shraga para post` is not.
77
+ if (args[0] === 'para' && args[1] === 'post') {
78
+ const { loadLinks } = await import('./server/para/feature.ts');
79
+ const { postProactive } = await import('./server/para/streamer.ts');
80
+
81
+ const links = loadLinks();
82
+ const wanted = flag('conn');
83
+ const ids = Object.keys(links);
84
+ // Refuse to guess. Picking "the first" would silently deliver a private report to whichever
85
+ // connection happened to sort first the day a second one is added.
86
+ const connId = wanted ?? (ids.length === 1 ? ids[0] : undefined);
87
+ if (!connId || !links[connId]) {
88
+ console.error(ids.length
89
+ ? `usage: shraga para post --conn <connId> (linked: ${ids.join(', ')})`
90
+ : 'no para link yet — send one message from para.li to this agent first, then retry.');
91
+ process.exit(1);
92
+ }
93
+ const link = links[connId];
94
+
95
+ const file = flag('file');
96
+ let text: string;
97
+ if (file) {
98
+ text = (await import('node:fs')).readFileSync(file, 'utf-8');
99
+ } else {
100
+ // A bare positional (not a flag, and not a flag's VALUE) is accepted for one-liners.
101
+ const flagVals = new Set<string>();
102
+ for (let i = 0; i < args.length; i++) if (args[i].startsWith('--')) flagVals.add(args[i + 1]);
103
+ const positional = args.slice(2).find((a) => !a.startsWith('--') && !flagVals.has(a));
104
+ text = positional ?? await new Response(Bun.stdin.stream()).text();
105
+ }
106
+ if (!text.trim()) {
107
+ console.error('nothing to post: text was empty (pipe it on stdin, pass --file, or give it as an argument)');
108
+ process.exit(1);
109
+ }
110
+
111
+ const ok = await postProactive({ url: link.url, secret: link.secret, connId }, link.convId, text);
112
+ // Exit code is the point: `postProactive` swallows transport failures into `false`, so a caller
113
+ // that only looked at stdout would read a silent drop as a successful delivery.
114
+ if (!ok) { console.error(`\u2716 para post FAILED \u2192 ${link.convId}`); process.exit(1); }
115
+ console.log(`\u2705 posted to ${link.convId}`);
116
+ process.exit(0);
117
+ }
118
+
62
119
  // `shraga ingress` — host-header TCP router for previews + blue-green flips.
63
120
  // Runs as its OWN process (INGRESS_PORT), deliberately separate from the server so it
64
121
  // survives server restarts during a flip. Reads dataPath('ingress-router.json').
@@ -50,9 +50,9 @@ const LINKS_PATH = dataPath('para-links.json');
50
50
  * taken from `validateApiKey`, never from the request body. A link written before this field
51
51
  * existed has no email and is therefore not an owner: it receives no notices until its next turn
52
52
  * refreshes the entry. */
53
- type Link = ParaCallback & { convId: string; at: number; uid: string; email?: string };
53
+ export type Link = ParaCallback & { convId: string; at: number; uid: string; email?: string };
54
54
 
55
- function loadLinks(): Record<string, Link> {
55
+ export function loadLinks(): Record<string, Link> {
56
56
  if (!existsSync(LINKS_PATH)) return {};
57
57
  try { return JSON.parse(readFileSync(LINKS_PATH, 'utf-8')); } catch (err) {
58
58
  console.warn('[para] links file unreadable, starting empty:', (err as Error).message);