nx_chat_demo_api.nx source
↩ module page · 430 lines · 18372 B
1// nx_chat_demo_api.nx -- the SAFE public front for a testable persistent-chat UI backed by C1's store.
2// v2 (2026-08-16): does the append/fetch IN-PROCESS on the seg-store, NOT by forking nx_chat_store.
3// WHY: MEASURED via curl to /mcp -- a list (read) returned in 44ms, but a post that FORKED nx_chat_store
4// to write hung to the tools-daemon exec deadline (~14s) and returned empty (exit -5), because the
5// write-path grandchild orphaned under the daemon's capture. A read fork was fine; a write fork was not.
6// In-process removes the fork entirely: this organ takes the plane lock, appends, and releases in its own
7// process -- the exact code path nx_chat_store's gate proves, just linked instead of exec'd. Fast + no orphan.
8// SECURITY BOUNDARY unchanged: only post/list, rooms forced into demo_, inputs sanitized + capped, budgets
9// from C1's conf refuse loudly. Destructive verbs do not exist here. REFEREE: nx_chat_demo_api_gate.
10// DEBT (named, not a silent twin): the append/fetch logic is duplicated from nx_chat_store; the consolidation
11// is to extract nx_chat_store_lib that BOTH this front and the CLI import (comms.plan chatstore-lib-extract).
12// license_tier: ORIGINAL No hw writes (Rule 26).
13import "nx_syscalls.nx"
14import "nx_itoa_lib.nx"
15import "nx_store_seed_lib.nx"
16import "nx_seg_store.nx"
17
18const CDA_EXIT_OK: i64 = 0
19const CDA_EXIT_USAGE: i64 = 2
20const CDA_EXIT_REFUSED: i64 = 3
21const CDA_ROOM_MAX: i64 = 24
22const CDA_NAME_MAX: i64 = 24
23const CDA_TEXT_MAX: i64 = 280
24const CDA_FETCH_MAX: i64 = 200
25const CDA_PFX_CAP: i64 = 256
26const CDA_ROW_OVH: i64 = 128
27const CDA_WSLACK: i64 = 65536
28const CDA_CONF_CAP: i64 = 192
29const CDA_OUTCAP: i64 = 262144
30const CDA_NL: i64 = 10
31const CDA_PIPE: i64 = 124
32const CDA_KIND_TEXT: i64 = 1
33const CDA_KIND_SYS: i64 = 4
34const CHATCONF: *u8 = "knowledge/comms/chatstore.conf"
35const CSA_ERR_CONF: i64 = 0 - 3
36const CSA_ERR_BODY: i64 = 0 - 10
37const CSA_ERR_LOCK: i64 = 0 - 4
38const CSA_ERR_MSGS: i64 = 0 - 11
39const CSA_ERR_BYTES: i64 = 0 - 12
40const CSA_ERR_CORRUPT: i64 = 0 - 5
41
42func cda_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
43func cda_wn(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 }
44func cda_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
45func cda_eq(a: *u8, b: *u8) -> i64 {
46 var i: i64 = 0
47 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
48 if b[i] != (0 as u8) { return 0 }
49 return 1
50}
51func cda_ident(src: *u8, dst: *u8, cap: i64) -> i64 {
52 var o: i64 = 0
53 var i: i64 = 0
54 while src[i] != (0 as u8) {
55 if o >= cap { break }
56 let c: i64 = src[i] as i64
57 if c >= 48 { if c <= 57 { dst[o] = c as u8; o = o + 1 } }
58 if c >= 97 { if c <= 122 { dst[o] = c as u8; o = o + 1 } }
59 if c >= 65 { if c <= 90 { dst[o] = (c + 32) as u8; o = o + 1 } }
60 if c == 95 { dst[o] = 95 as u8; o = o + 1 }
61 i = i + 1
62 }
63 dst[o] = 0 as u8
64 return o
65}
66func cda_text(src: *u8, dst: *u8, cap: i64) -> i64 {
67 var o: i64 = 0
68 var i: i64 = 0
69 while src[i] != (0 as u8) {
70 if o >= cap { break }
71 let c: i64 = src[i] as i64
72 if c >= 32 { if c <= 126 { dst[o] = c as u8; o = o + 1 } }
73 i = i + 1
74 }
75 dst[o] = 0 as u8
76 return o
77}
78func cda_demoroom(clean: *u8, out: *u8) -> i64 {
79 var o: i64 = 0
80 let pre: *u8 = "demo_" as *u8
81 while pre[o] != (0 as u8) { out[o] = pre[o]; o = o + 1 }
82 var i: i64 = 0
83 while clean[i] != (0 as u8) { out[o] = clean[i]; o = o + 1; i = i + 1 }
84 out[o] = 0 as u8
85 return o
86}
87func cda_jesc(out: *u8, o: i64, s: *u8, n: i64) -> i64 {
88 var i: i64 = 0
89 while i < n {
90 let c: i64 = s[i] as i64
91 if c == 34 { out[o] = 92 as u8; o = o + 1; out[o] = 34 as u8; o = o + 1 } else {
92 if c == 92 { out[o] = 92 as u8; o = o + 1; out[o] = 92 as u8; o = o + 1 } else {
93 if c < 32 { out[o] = 32 as u8; o = o + 1 } else {
94 out[o] = c as u8; o = o + 1
95 } } }
96 i = i + 1
97 }
98 return o
99}
100func cda_hexe(src: *u8, n: i64, dst: *u8) -> i64 {
101 var i: i64 = 0
102 while i < n {
103 let b: i64 = src[i] as i64
104 let hi: i64 = b / 16
105 let lo: i64 = b % 16
106 if hi < 10 { dst[i*2] = (48 + hi) as u8 } else { dst[i*2] = (87 + hi) as u8 }
107 if lo < 10 { dst[i*2+1] = (48 + lo) as u8 } else { dst[i*2+1] = (87 + lo) as u8 }
108 i = i + 1
109 }
110 return n * 2
111}
112func cda_hexv(c: i64) -> i64 {
113 if c >= 48 { if c <= 57 { return c - 48 } }
114 if c >= 97 { if c <= 102 { return c - 87 } }
115 return 0 - 1
116}
117func cda_catn(d: *u8, o: i64, v: i64) -> i64 {
118 if v == 0 { d[o] = 48 as u8; return o + 1 }
119 let tmp: *u8 = sys_mmap(24)
120 var k: i64 = 0
121 var m: i64 = v
122 while m > 0 { tmp[k] = (48 + m % 10) as u8; m = m / 10; k = k + 1 }
123 var j: i64 = k - 1
124 while j >= 0 { d[o] = tmp[j]; o = o + 1; j = j - 1 }
125 return o
126}
127func cda_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o }
128func cda_confs(key: *u8, out: *u8, cap: i64) -> i64 {
129 let ol: *i64 = sts_mm(16) as *i64
130 let b: *u8 = sys_read_file(CHATCONF, ol)
131 let n: i64 = ol[0]
132 if b as i64 == 0 { return 0 - 1 }
133 if n <= 0 { return 0 - 1 }
134 let kl: i64 = cda_len(key)
135 var i: i64 = 0
136 while i < n {
137 var j: i64 = 0
138 var hit: i64 = 1
139 while j < kl {
140 if i + j >= n { hit = 0; break }
141 if b[i+j] != key[j] { hit = 0; break }
142 j = j + 1
143 }
144 if hit == 1 { if i + kl >= n { hit = 0 } else { if b[i+kl] != (CDA_PIPE as u8) { hit = 0 } } }
145 if hit == 1 {
146 var v: i64 = i + kl + 1
147 var o: i64 = 0
148 while v < n { if b[v] == (CDA_NL as u8) { break } if o < cap - 1 { out[o] = b[v]; o = o + 1 } v = v + 1 }
149 out[o] = 0 as u8
150 return o
151 }
152 while i < n { if b[i] == (CDA_NL as u8) { break } i = i + 1 }
153 i = i + 1
154 }
155 return 0 - 1
156}
157func cda_confn(key: *u8) -> i64 {
158 let v: *u8 = sts_mm(CDA_CONF_CAP)
159 let l: i64 = cda_confs(key, v, CDA_CONF_CAP)
160 if l <= 0 { return 0 - 1 }
161 return sts_atoi(v, l)
162}
163func cda_prefix(room: *u8, out: *u8) -> i64 {
164 let rl: i64 = cda_confs("store_root" as *u8, out, CDA_PFX_CAP - 96)
165 if rl <= 0 { return 0 - 2 }
166 var o: i64 = rl
167 o = ss_cat(out, o, "chat_" as *u8)
168 o = ss_cat(out, o, room)
169 o = ss_cat(out, o, "-" as *u8)
170 out[o] = 0 as u8
171 return o
172}
173func cda_rowbuild(seq: i64, kind: i64, sender: *u8, body: *u8, blen: i64, out: *u8) -> i64 {
174 var o: i64 = ss_cat(out, 0, "m|" as *u8)
175 o = ss_catn(out, o, seq)
176 o = ss_cat(out, o, "|" as *u8)
177 o = ss_catn(out, o, sys_now_realtime_ms())
178 o = ss_cat(out, o, "|" as *u8)
179 o = ss_catn(out, o, kind)
180 o = ss_cat(out, o, "|" as *u8)
181 o = ss_cat(out, o, sender)
182 o = ss_cat(out, o, "|" as *u8)
183 o = o + cda_hexe(body, blen, (out as i64 + o) as *u8)
184 out[o] = 0 as u8
185 return o
186}
187func cda_delkey(seq: i64, out: *u8) -> i64 {
188 var o: i64 = ss_cat(out, 0, "del:" as *u8)
189 o = ss_catn(out, o, seq)
190 out[o] = 0 as u8
191 return o
192}
193func cda_put_key(prefix: *u8, key: *u8, val: *u8, vlen: i64) -> i64 {
194 let w: *i64 = ss_begin_cap(vlen + CDA_WSLACK)
195 if ss_add(w, 1, key, val, vlen) < 0 { return 0 - 1 }
196 if ss_commit_cas(prefix, w, ss_next_segid(prefix), SS_CAS_ANY) != 0 { return 0 - 1 }
197 return 0
198}
199func csa_append(room: *u8, sender: *u8, body: *u8, blen: i64) -> i64 {
200 let bmax: i64 = cda_confn("body_bytes_max" as *u8)
201 if bmax < 0 { return CSA_ERR_CONF }
202 let bcliff: i64 = cda_confn("room_bytes_cliff" as *u8)
203 if bcliff < 0 { return CSA_ERR_CONF }
204 let mcliff: i64 = cda_confn("room_msgs_cliff" as *u8)
205 if mcliff < 0 { return CSA_ERR_CONF }
206 if blen > bmax { return CSA_ERR_BODY }
207 let pfx: *u8 = sts_mm(CDA_PFX_CAP)
208 if cda_prefix(room, pfx) < 0 { return CSA_ERR_CONF }
209 let lk: i64 = sts_lock(pfx)
210 if lk < 0 { return CSA_ERR_LOCK }
211 let pq: *i64 = sts_mm(16) as *i64
212 let lq: *i64 = sts_mm(16) as *i64
213 var n: i64 = 0
214 if ss_get(pfx, "q:n" as *u8, pq, lq) == 1 {
215 n = sts_atoi(pq[0] as *u8, lq[0])
216 } else {
217 // auto-open with a DEFERRED commit too (no fsync): write q:0=sysrow, q:n=1, meta:bytes in one
218 // segment. The old path used sts_seed which fsyncs, and under disk pressure that stalled past the
219 // caller's deadline BEFORE the room was created, so every retry re-stalled. Deferred fixes it.
220 let ob: *u8 = "room-open" as *u8
221 let obl: i64 = 9
222 let srow: *u8 = sts_mm(CDA_ROW_OVH + 64 + obl * 2 + 8)
223 let so: i64 = cda_rowbuild(1, CDA_KIND_SYS, sender, ob, obl, srow)
224 let k0: *u8 = sts_mm(64)
225 sts_rowkey(0, k0)
226 let c1: *u8 = sts_mm(32)
227 let cl1: i64 = ss_catn(c1, 0, 1)
228 let m0: *u8 = sts_mm(32)
229 let ml0: i64 = ss_catn(m0, 0, obl)
230 let ow: *i64 = ss_begin_cap(so + CDA_WSLACK)
231 if ss_add(ow, 1, k0, srow, so) < 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
232 if ss_add(ow, 1, "q:n" as *u8, c1, cl1) < 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
233 if ss_add(ow, 1, "meta:bytes" as *u8, m0, ml0) < 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
234 if ss_commit_deferred(pfx, ow, ss_next_segid(pfx)) != 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
235 n = 1
236 }
237 if n >= mcliff { sts_unlock(lk) return CSA_ERR_MSGS }
238 var bts: i64 = 0
239 if ss_get(pfx, "meta:bytes" as *u8, pq, lq) == 1 { bts = sts_atoi(pq[0] as *u8, lq[0]) }
240 if bts + blen > bcliff { sts_unlock(lk) return CSA_ERR_BYTES }
241 let seq: i64 = n + 1
242 let row: *u8 = sts_mm(CDA_ROW_OVH + 64 + blen * 2 + 8)
243 let rl: i64 = cda_rowbuild(seq, CDA_KIND_TEXT, sender, body, blen, row)
244 let key: *u8 = sts_mm(64)
245 sts_rowkey(n, key)
246 let cb: *u8 = sts_mm(32)
247 let cl: i64 = ss_catn(cb, 0, seq)
248 let mb: *u8 = sts_mm(32)
249 let ml: i64 = ss_catn(mb, 0, bts + blen)
250 let w: *i64 = ss_begin_cap(rl + CDA_WSLACK)
251 if ss_add(w, 1, key, row, rl) < 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
252 if ss_add(w, 1, "q:n" as *u8, cb, cl) < 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
253 if ss_add(w, 1, "meta:bytes" as *u8, mb, ml) < 0 { sts_unlock(lk) return CSA_ERR_CORRUPT }
254 // DEFERRED COMMIT (perf): write the segment + manifest to the page cache and return -- the message is
255 // visible to the next read immediately and survives process death; the fsync to disk happens on the
256 // store's background sync. We hold the plane flock across the whole read-modify-write, so no CAS is
257 // needed (nothing else can commit between our ss_get and this commit). This keeps a post FAST even
258 // when the box's disk is under pressure, instead of stalling on a synchronous fsync.
259 let rc: i64 = ss_commit_deferred(pfx, w, ss_next_segid(pfx))
260 sts_unlock(lk)
261 if rc != 0 { return CSA_ERR_CORRUPT }
262 return seq
263}
264func csa_fetch(room: *u8, since: i64, maxn: i64, outbuf: *u8, outcap: i64) -> i64 {
265 let pfx: *u8 = sts_mm(CDA_PFX_CAP)
266 if cda_prefix(room, pfx) < 0 { return 0 - 1 }
267 let pq: *i64 = sts_mm(16) as *i64
268 let lq: *i64 = sts_mm(16) as *i64
269 if ss_get(pfx, "q:n" as *u8, pq, lq) != 1 { return 0 - 1 }
270 let n: i64 = sts_atoi(pq[0] as *u8, lq[0])
271 let h: *i64 = ss_open_cached(pfx)
272 if h as i64 == 0 { return 0 - 5 }
273 let key: *u8 = sts_mm(64)
274 let dk: *u8 = sts_mm(64)
275 let pr2: *i64 = sts_mm(16) as *i64
276 let lr2: *i64 = sts_mm(16) as *i64
277 var o: i64 = 0
278 var returned: i64 = 0
279 var i: i64 = since
280 while i < n {
281 if returned >= maxn { break }
282 sts_rowkey(i, key)
283 if ss_hget(h, key, pq, lq) != 1 { return 0 - 5 }
284 let rv: *u8 = pq[0] as *u8
285 let rl: i64 = lq[0]
286 cda_delkey(i + 1, dk)
287 if ss_hget(h, dk, pr2, lr2) == 1 { } else {
288 if o + rl + 1 < outcap {
289 var k: i64 = 0
290 while k < rl { outbuf[o] = rv[k]; o = o + 1; k = k + 1 }
291 outbuf[o] = CDA_NL as u8
292 o = o + 1
293 returned = returned + 1
294 }
295 }
296 i = i + 1
297 }
298 return o
299}
300func cda_post(room_in: *u8, name_in: *u8, text_in: *u8) -> i64 {
301 let rclean: *u8 = sts_mm(64)
302 if cda_ident(room_in, rclean, CDA_ROOM_MAX) < 1 { cda_w("{\"ok\":0,\"err\":\"bad-room\"}\n" as *u8) return CDA_EXIT_REFUSED }
303 let nclean: *u8 = sts_mm(64)
304 if cda_ident(name_in, nclean, CDA_NAME_MAX) < 1 { cda_w("{\"ok\":0,\"err\":\"bad-name\"}\n" as *u8) return CDA_EXIT_REFUSED }
305 let tclean: *u8 = sts_mm(CDA_TEXT_MAX + 8)
306 let tlen: i64 = cda_text(text_in, tclean, CDA_TEXT_MAX)
307 if tlen < 1 { cda_w("{\"ok\":0,\"err\":\"empty-text\"}\n" as *u8) return CDA_EXIT_REFUSED }
308 let room: *u8 = sts_mm(64)
309 cda_demoroom(rclean, room)
310 let seq: i64 = csa_append(room, nclean, tclean, tlen)
311 if seq < 0 {
312 if seq == CSA_ERR_MSGS { cda_w("{\"ok\":0,\"err\":\"room-full-messages\"}\n" as *u8) return CDA_EXIT_REFUSED }
313 if seq == CSA_ERR_BYTES { cda_w("{\"ok\":0,\"err\":\"room-full-bytes\"}\n" as *u8) return CDA_EXIT_REFUSED }
314 if seq == CSA_ERR_BODY { cda_w("{\"ok\":0,\"err\":\"body-too-big\"}\n" as *u8) return CDA_EXIT_REFUSED }
315 if seq == CSA_ERR_LOCK { cda_w("{\"ok\":0,\"err\":\"busy-retry\"}\n" as *u8) return CDA_EXIT_REFUSED }
316 cda_w("{\"ok\":0,\"err\":\"append-failed\"}\n" as *u8)
317 return CDA_EXIT_REFUSED
318 }
319 let jb: *u8 = sts_mm(256)
320 var o: i64 = cda_cat(jb, 0, "{\"ok\":1,\"room\":\"" as *u8)
321 o = cda_cat(jb, o, room)
322 o = cda_cat(jb, o, "\",\"seq\":" as *u8)
323 o = cda_catn(jb, o, seq)
324 o = cda_cat(jb, o, "}\n" as *u8)
325 cda_wn(jb, o)
326 return CDA_EXIT_OK
327}
328func cda_atoi(b: *u8, n: i64) -> i64 { var v: i64 = 0; var i: i64 = 0; while i < n { let c: i64 = b[i] as i64; if c < 48 { break } if c > 57 { break } v = v * 10 + (c - 48); i = i + 1 } return v }
329func cda_list(room_in: *u8, since_in: *u8) -> i64 {
330 let rclean: *u8 = sts_mm(64)
331 if cda_ident(room_in, rclean, CDA_ROOM_MAX) < 1 { cda_w("{\"ok\":0,\"err\":\"bad-room\"}\n" as *u8) return CDA_EXIT_REFUSED }
332 let room: *u8 = sts_mm(64)
333 cda_demoroom(rclean, room)
334 var since: i64 = 0
335 if since_in as i64 != 0 { since = cda_atoi(since_in, cda_len(since_in)) }
336 let raw: *u8 = sts_mm(CDA_OUTCAP)
337 let rn: i64 = csa_fetch(room, since, CDA_FETCH_MAX, raw, CDA_OUTCAP)
338 let jb: *u8 = sts_mm(CDA_OUTCAP)
339 var o: i64 = cda_cat(jb, 0, "{\"ok\":1,\"room\":\"" as *u8)
340 o = cda_cat(jb, o, room)
341 o = cda_cat(jb, o, "\",\"msgs\":[" as *u8)
342 var total: i64 = 0
343 if rn > 0 {
344 var i: i64 = 0
345 var first: i64 = 1
346 while i < rn {
347 var ls: i64 = i
348 while i < rn { if raw[i] == (CDA_NL as u8) { break } i = i + 1 }
349 let le: i64 = i
350 i = i + 1
351 if le - ls > 4 { if raw[ls] == (109 as u8) { if raw[ls+1] == (CDA_PIPE as u8) {
352 var p: i64 = ls + 2
353 var seq: i64 = 0
354 while p < le { let c: i64 = raw[p] as i64; if c == CDA_PIPE { break } seq = seq * 10 + (c - 48); p = p + 1 }
355 p = p + 1
356 var ep: i64 = 0
357 while p < le { let c2: i64 = raw[p] as i64; if c2 == CDA_PIPE { break } ep = ep * 10 + (c2 - 48); p = p + 1 }
358 p = p + 1
359 var kind: i64 = 0
360 while p < le { let c3: i64 = raw[p] as i64; if c3 == CDA_PIPE { break } kind = kind * 10 + (c3 - 48); p = p + 1 }
361 p = p + 1
362 let snds: i64 = p
363 while p < le { if raw[p] == (CDA_PIPE as u8) { break } p = p + 1 }
364 let snde: i64 = p
365 p = p + 1
366 let hxs: i64 = p
367 if kind == CDA_KIND_TEXT {
368 let hxlen: i64 = le - hxs
369 let bodyb: *u8 = sts_mm(hxlen / 2 + 4)
370 var bo: i64 = 0
371 var hh: i64 = hxs
372 var okhex: i64 = 1
373 while hh + 1 < le {
374 let hi: i64 = cda_hexv(raw[hh] as i64)
375 let lo: i64 = cda_hexv(raw[hh+1] as i64)
376 if hi < 0 { okhex = 0; break }
377 if lo < 0 { okhex = 0; break }
378 bodyb[bo] = (hi * 16 + lo) as u8
379 bo = bo + 1
380 hh = hh + 2
381 }
382 if okhex == 1 {
383 if first == 0 { jb[o] = 44 as u8; o = o + 1 }
384 first = 0
385 o = cda_cat(jb, o, "{\"seq\":" as *u8)
386 o = cda_catn(jb, o, seq)
387 o = cda_cat(jb, o, ",\"t\":" as *u8)
388 o = cda_catn(jb, o, ep)
389 o = cda_cat(jb, o, ",\"from\":\"" as *u8)
390 o = cda_jesc(jb, o, (raw as i64 + snds) as *u8, snde - snds)
391 o = cda_cat(jb, o, "\",\"text\":\"" as *u8)
392 o = cda_jesc(jb, o, bodyb, bo)
393 o = cda_cat(jb, o, "\"}" as *u8)
394 total = total + 1
395 }
396 }
397 } } }
398 }
399 }
400 o = cda_cat(jb, o, "],\"total\":" as *u8)
401 o = cda_catn(jb, o, total)
402 o = cda_cat(jb, o, "}\n" as *u8)
403 cda_wn(jb, o)
404 return CDA_EXIT_OK
405}
406func cda_usage() -> i64 {
407 cda_w("{\"ok\":0,\"err\":\"usage: post <room> <name> <text> | list <room> [since]\"}\n" as *u8)
408 return CDA_EXIT_USAGE
409}
410func main(argc: i64, argv: *i64) -> i64 {
411 if argc < 2 { let u: i64 = cda_usage() sys_exit(u) return u }
412 let v: *u8 = argv[1] as *u8
413 if cda_eq(v, "post" as *u8) == 1 {
414 if argc < 5 { let u: i64 = cda_usage() sys_exit(u) return u }
415 let r: i64 = cda_post(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8)
416 sys_exit(r)
417 return r
418 }
419 if cda_eq(v, "list" as *u8) == 1 {
420 if argc < 3 { let u: i64 = cda_usage() sys_exit(u) return u }
421 var since: *u8 = 0 as *u8
422 if argc > 3 { since = argv[3] as *u8 }
423 let r: i64 = cda_list(argv[2] as *u8, since)
424 sys_exit(r)
425 return r
426 }
427 let u2: i64 = cda_usage()
428 sys_exit(u2)
429 return u2
430}