hacklab 0.22.3 → 26.921.1

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 (259) hide show
  1. package/README.md +23 -430
  2. package/dist/index.js +132 -23
  3. package/package.json +20 -42
  4. package/LICENSE +0 -21
  5. package/dist/agent-handoff.d.ts +0 -51
  6. package/dist/agent-handoff.d.ts.map +0 -1
  7. package/dist/agent-handoff.js +0 -91
  8. package/dist/agent-handoff.js.map +0 -1
  9. package/dist/api-client.d.ts +0 -18
  10. package/dist/api-client.d.ts.map +0 -1
  11. package/dist/api-client.js +0 -40
  12. package/dist/api-client.js.map +0 -1
  13. package/dist/belt.d.ts +0 -25
  14. package/dist/belt.d.ts.map +0 -1
  15. package/dist/belt.js +0 -113
  16. package/dist/belt.js.map +0 -1
  17. package/dist/card.d.ts +0 -66
  18. package/dist/card.d.ts.map +0 -1
  19. package/dist/card.js +0 -151
  20. package/dist/card.js.map +0 -1
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/cli.d.ts.map +0 -1
  23. package/dist/cli.js +0 -238
  24. package/dist/cli.js.map +0 -1
  25. package/dist/commands/book.d.ts +0 -14
  26. package/dist/commands/book.d.ts.map +0 -1
  27. package/dist/commands/book.js +0 -90
  28. package/dist/commands/book.js.map +0 -1
  29. package/dist/commands/chat.d.ts +0 -36
  30. package/dist/commands/chat.d.ts.map +0 -1
  31. package/dist/commands/chat.js +0 -642
  32. package/dist/commands/chat.js.map +0 -1
  33. package/dist/commands/config.d.ts +0 -2
  34. package/dist/commands/config.d.ts.map +0 -1
  35. package/dist/commands/config.js +0 -84
  36. package/dist/commands/config.js.map +0 -1
  37. package/dist/commands/daemon.d.ts +0 -19
  38. package/dist/commands/daemon.d.ts.map +0 -1
  39. package/dist/commands/daemon.js +0 -72
  40. package/dist/commands/daemon.js.map +0 -1
  41. package/dist/commands/drop.d.ts +0 -12
  42. package/dist/commands/drop.d.ts.map +0 -1
  43. package/dist/commands/drop.js +0 -103
  44. package/dist/commands/drop.js.map +0 -1
  45. package/dist/commands/drops.d.ts +0 -9
  46. package/dist/commands/drops.d.ts.map +0 -1
  47. package/dist/commands/drops.js +0 -96
  48. package/dist/commands/drops.js.map +0 -1
  49. package/dist/commands/essay.d.ts +0 -40
  50. package/dist/commands/essay.d.ts.map +0 -1
  51. package/dist/commands/essay.js +0 -651
  52. package/dist/commands/essay.js.map +0 -1
  53. package/dist/commands/hackathon.d.ts +0 -2
  54. package/dist/commands/hackathon.d.ts.map +0 -1
  55. package/dist/commands/hackathon.js +0 -893
  56. package/dist/commands/hackathon.js.map +0 -1
  57. package/dist/commands/hacker.d.ts +0 -2
  58. package/dist/commands/hacker.d.ts.map +0 -1
  59. package/dist/commands/hacker.js +0 -87
  60. package/dist/commands/hacker.js.map +0 -1
  61. package/dist/commands/jobs.d.ts +0 -18
  62. package/dist/commands/jobs.d.ts.map +0 -1
  63. package/dist/commands/jobs.js +0 -138
  64. package/dist/commands/jobs.js.map +0 -1
  65. package/dist/commands/keys.d.ts +0 -2
  66. package/dist/commands/keys.d.ts.map +0 -1
  67. package/dist/commands/keys.js +0 -138
  68. package/dist/commands/keys.js.map +0 -1
  69. package/dist/commands/login.d.ts +0 -83
  70. package/dist/commands/login.d.ts.map +0 -1
  71. package/dist/commands/login.js +0 -236
  72. package/dist/commands/login.js.map +0 -1
  73. package/dist/commands/logout.d.ts +0 -2
  74. package/dist/commands/logout.d.ts.map +0 -1
  75. package/dist/commands/logout.js +0 -22
  76. package/dist/commands/logout.js.map +0 -1
  77. package/dist/commands/org-jobs.d.ts +0 -50
  78. package/dist/commands/org-jobs.d.ts.map +0 -1
  79. package/dist/commands/org-jobs.js +0 -383
  80. package/dist/commands/org-jobs.js.map +0 -1
  81. package/dist/commands/org.d.ts +0 -67
  82. package/dist/commands/org.d.ts.map +0 -1
  83. package/dist/commands/org.js +0 -1244
  84. package/dist/commands/org.js.map +0 -1
  85. package/dist/commands/ping.d.ts +0 -2
  86. package/dist/commands/ping.d.ts.map +0 -1
  87. package/dist/commands/ping.js +0 -50
  88. package/dist/commands/ping.js.map +0 -1
  89. package/dist/commands/profile.d.ts +0 -72
  90. package/dist/commands/profile.d.ts.map +0 -1
  91. package/dist/commands/profile.js +0 -672
  92. package/dist/commands/profile.js.map +0 -1
  93. package/dist/commands/project.d.ts +0 -2
  94. package/dist/commands/project.d.ts.map +0 -1
  95. package/dist/commands/project.js +0 -583
  96. package/dist/commands/project.js.map +0 -1
  97. package/dist/commands/referral.d.ts +0 -6
  98. package/dist/commands/referral.d.ts.map +0 -1
  99. package/dist/commands/referral.js +0 -93
  100. package/dist/commands/referral.js.map +0 -1
  101. package/dist/commands/rtfm.d.ts +0 -9
  102. package/dist/commands/rtfm.d.ts.map +0 -1
  103. package/dist/commands/rtfm.js +0 -114
  104. package/dist/commands/rtfm.js.map +0 -1
  105. package/dist/commands/scan.d.ts +0 -13
  106. package/dist/commands/scan.d.ts.map +0 -1
  107. package/dist/commands/scan.js +0 -286
  108. package/dist/commands/scan.js.map +0 -1
  109. package/dist/commands/scout.d.ts +0 -18
  110. package/dist/commands/scout.d.ts.map +0 -1
  111. package/dist/commands/scout.js +0 -232
  112. package/dist/commands/scout.js.map +0 -1
  113. package/dist/commands/setup-rail.d.ts +0 -58
  114. package/dist/commands/setup-rail.d.ts.map +0 -1
  115. package/dist/commands/setup-rail.js +0 -135
  116. package/dist/commands/setup-rail.js.map +0 -1
  117. package/dist/commands/setup.d.ts +0 -29
  118. package/dist/commands/setup.d.ts.map +0 -1
  119. package/dist/commands/setup.js +0 -442
  120. package/dist/commands/setup.js.map +0 -1
  121. package/dist/commands/sync.d.ts +0 -9
  122. package/dist/commands/sync.d.ts.map +0 -1
  123. package/dist/commands/sync.js +0 -345
  124. package/dist/commands/sync.js.map +0 -1
  125. package/dist/commands/update.d.ts +0 -2
  126. package/dist/commands/update.d.ts.map +0 -1
  127. package/dist/commands/update.js +0 -92
  128. package/dist/commands/update.js.map +0 -1
  129. package/dist/commands/whoami.d.ts +0 -2
  130. package/dist/commands/whoami.d.ts.map +0 -1
  131. package/dist/commands/whoami.js +0 -41
  132. package/dist/commands/whoami.js.map +0 -1
  133. package/dist/config.d.ts +0 -78
  134. package/dist/config.d.ts.map +0 -1
  135. package/dist/config.js +0 -93
  136. package/dist/config.js.map +0 -1
  137. package/dist/daily-sync.d.ts +0 -150
  138. package/dist/daily-sync.d.ts.map +0 -1
  139. package/dist/daily-sync.js +0 -665
  140. package/dist/daily-sync.js.map +0 -1
  141. package/dist/dossier.d.ts +0 -73
  142. package/dist/dossier.d.ts.map +0 -1
  143. package/dist/dossier.js +0 -141
  144. package/dist/dossier.js.map +0 -1
  145. package/dist/field-normalize.d.ts +0 -4
  146. package/dist/field-normalize.d.ts.map +0 -1
  147. package/dist/field-normalize.js +0 -8
  148. package/dist/field-normalize.js.map +0 -1
  149. package/dist/index.d.ts +0 -3
  150. package/dist/index.d.ts.map +0 -1
  151. package/dist/index.js.map +0 -1
  152. package/dist/job-fields.d.ts +0 -47
  153. package/dist/job-fields.d.ts.map +0 -1
  154. package/dist/job-fields.js +0 -148
  155. package/dist/job-fields.js.map +0 -1
  156. package/dist/machine.d.ts +0 -15
  157. package/dist/machine.d.ts.map +0 -1
  158. package/dist/machine.js +0 -64
  159. package/dist/machine.js.map +0 -1
  160. package/dist/org-fields.d.ts +0 -57
  161. package/dist/org-fields.d.ts.map +0 -1
  162. package/dist/org-fields.js +0 -167
  163. package/dist/org-fields.js.map +0 -1
  164. package/dist/posthog.d.ts +0 -38
  165. package/dist/posthog.d.ts.map +0 -1
  166. package/dist/posthog.js +0 -224
  167. package/dist/posthog.js.map +0 -1
  168. package/dist/project-fields.d.ts +0 -40
  169. package/dist/project-fields.d.ts.map +0 -1
  170. package/dist/project-fields.js +0 -79
  171. package/dist/project-fields.js.map +0 -1
  172. package/dist/prompt-consent.d.ts +0 -73
  173. package/dist/prompt-consent.d.ts.map +0 -1
  174. package/dist/prompt-consent.js +0 -191
  175. package/dist/prompt-consent.js.map +0 -1
  176. package/dist/prompt-stats.d.ts +0 -226
  177. package/dist/prompt-stats.d.ts.map +0 -1
  178. package/dist/prompt-stats.js +0 -561
  179. package/dist/prompt-stats.js.map +0 -1
  180. package/dist/referral.d.ts +0 -14
  181. package/dist/referral.d.ts.map +0 -1
  182. package/dist/referral.js +0 -28
  183. package/dist/referral.js.map +0 -1
  184. package/dist/registry.d.ts +0 -15
  185. package/dist/registry.d.ts.map +0 -1
  186. package/dist/registry.js +0 -180
  187. package/dist/registry.js.map +0 -1
  188. package/dist/resolve-command.d.ts +0 -20
  189. package/dist/resolve-command.d.ts.map +0 -1
  190. package/dist/resolve-command.js +0 -21
  191. package/dist/resolve-command.js.map +0 -1
  192. package/dist/scanners/antigravity.d.ts +0 -41
  193. package/dist/scanners/antigravity.d.ts.map +0 -1
  194. package/dist/scanners/antigravity.js +0 -210
  195. package/dist/scanners/antigravity.js.map +0 -1
  196. package/dist/scanners/github-copilot-telemetry.d.ts +0 -23
  197. package/dist/scanners/github-copilot-telemetry.d.ts.map +0 -1
  198. package/dist/scanners/github-copilot-telemetry.js +0 -220
  199. package/dist/scanners/github-copilot-telemetry.js.map +0 -1
  200. package/dist/scanners/github-copilot.d.ts +0 -68
  201. package/dist/scanners/github-copilot.d.ts.map +0 -1
  202. package/dist/scanners/github-copilot.js +0 -262
  203. package/dist/scanners/github-copilot.js.map +0 -1
  204. package/dist/scanners/incremental.d.ts +0 -282
  205. package/dist/scanners/incremental.d.ts.map +0 -1
  206. package/dist/scanners/incremental.js +0 -788
  207. package/dist/scanners/incremental.js.map +0 -1
  208. package/dist/scanners/index.d.ts +0 -116
  209. package/dist/scanners/index.d.ts.map +0 -1
  210. package/dist/scanners/index.js +0 -726
  211. package/dist/scanners/index.js.map +0 -1
  212. package/dist/scanners/sqlite.d.ts +0 -18
  213. package/dist/scanners/sqlite.d.ts.map +0 -1
  214. package/dist/scanners/sqlite.js +0 -63
  215. package/dist/scanners/sqlite.js.map +0 -1
  216. package/dist/scanners/util.d.ts +0 -112
  217. package/dist/scanners/util.d.ts.map +0 -1
  218. package/dist/scanners/util.js +0 -167
  219. package/dist/scanners/util.js.map +0 -1
  220. package/dist/session.d.ts +0 -106
  221. package/dist/session.d.ts.map +0 -1
  222. package/dist/session.js +0 -191
  223. package/dist/session.js.map +0 -1
  224. package/dist/share-card-colors.d.ts +0 -19
  225. package/dist/share-card-colors.d.ts.map +0 -1
  226. package/dist/share-card-colors.js +0 -36
  227. package/dist/share-card-colors.js.map +0 -1
  228. package/dist/share-card.d.ts +0 -31
  229. package/dist/share-card.d.ts.map +0 -1
  230. package/dist/share-card.js +0 -380
  231. package/dist/share-card.js.map +0 -1
  232. package/dist/share.d.ts +0 -17
  233. package/dist/share.d.ts.map +0 -1
  234. package/dist/share.js +0 -75
  235. package/dist/share.js.map +0 -1
  236. package/dist/sync.d.ts +0 -100
  237. package/dist/sync.d.ts.map +0 -1
  238. package/dist/sync.js +0 -304
  239. package/dist/sync.js.map +0 -1
  240. package/dist/ui.d.ts +0 -33
  241. package/dist/ui.d.ts.map +0 -1
  242. package/dist/ui.js +0 -127
  243. package/dist/ui.js.map +0 -1
  244. package/dist/utils/npmGlobal.d.ts +0 -30
  245. package/dist/utils/npmGlobal.d.ts.map +0 -1
  246. package/dist/utils/npmGlobal.js +0 -69
  247. package/dist/utils/npmGlobal.js.map +0 -1
  248. package/dist/utils/openBrowser.d.ts +0 -14
  249. package/dist/utils/openBrowser.d.ts.map +0 -1
  250. package/dist/utils/openBrowser.js +0 -47
  251. package/dist/utils/openBrowser.js.map +0 -1
  252. package/dist/utils/updateCheck.d.ts +0 -16
  253. package/dist/utils/updateCheck.d.ts.map +0 -1
  254. package/dist/utils/updateCheck.js +0 -111
  255. package/dist/utils/updateCheck.js.map +0 -1
  256. package/dist/utils/waitForEnter.d.ts +0 -12
  257. package/dist/utils/waitForEnter.d.ts.map +0 -1
  258. package/dist/utils/waitForEnter.js +0 -43
  259. package/dist/utils/waitForEnter.js.map +0 -1
@@ -1,642 +0,0 @@
1
- import readline from 'node:readline';
2
- import { renderCompact } from '../card.js';
3
- import { captureEvent } from '../posthog.js';
4
- import { loadSession, resolveAppUrl } from '../session.js';
5
- import { bold, dim, error, green, info, rankColor, red, stripControl, success, } from '../ui.js';
6
- // Re-exported for existing importers (chat.test.ts); the implementation moved
7
- // to ui.ts so any command can strip control chars from untrusted output.
8
- export { stripControl } from '../ui.js';
9
- // The chat command is CLI-first: every subcommand takes --json so an AI agent
10
- // can drive the channel and DMs programmatically, while the default output is a
11
- // human-readable terminal view. Thin wrappers over the /api/chat/* routes.
12
- const CHANNEL_BASE = '/api/chat/channel/main';
13
- function printJson(data) {
14
- console.log(JSON.stringify(data, null, 2));
15
- }
16
- // The session-aware backend resolver (see resolveAppUrl for the precedence).
17
- // Kept as a thin named export so the chat call sites and tests read clearly.
18
- export function baseUrl(session) {
19
- return resolveAppUrl(session);
20
- }
21
- async function requireSession() {
22
- const session = await loadSession();
23
- if (!session) {
24
- error('not logged in');
25
- info(`run ${dim('hacklab login')} first`);
26
- process.exit(1);
27
- }
28
- return session;
29
- }
30
- async function readError(res) {
31
- const data = (await res.json().catch(() => null));
32
- return data?.error ?? `request failed (${res.status})`;
33
- }
34
- function flagValue(args, name) {
35
- const i = args.indexOf(name);
36
- if (i !== -1 && args[i + 1])
37
- return args[i + 1];
38
- const eq = args.find((a) => a.startsWith(`${name}=`));
39
- return eq ? eq.slice(name.length + 1) : undefined;
40
- }
41
- // Universal, locale-independent labels — we never want American month/day
42
- // ordering or localized names sneaking in. Lowercase to match the terminal's
43
- // quiet aesthetic. Indexed by Date.getDay() (0 = Sunday) / Date.getMonth().
44
- const WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
45
- const MONTHS = [
46
- 'jan',
47
- 'feb',
48
- 'mar',
49
- 'apr',
50
- 'may',
51
- 'jun',
52
- 'jul',
53
- 'aug',
54
- 'sep',
55
- 'oct',
56
- 'nov',
57
- 'dec',
58
- ];
59
- function pad2(n) {
60
- return String(n).padStart(2, '0');
61
- }
62
- // A calendar-day index in *local* time: the count of days since the epoch using
63
- // the date's local Y/M/D. Computing it via Date.UTC on the local components
64
- // strips the time-of-day entirely, so subtracting two of these gives an exact
65
- // whole-day difference that is immune to DST (a local "day" can be 23 or 25
66
- // hours, but we never look at hours) and to leap years (Date.UTC normalizes
67
- // real calendar dates). This is the basis for "today vs yesterday vs N days
68
- // ago" — never a raw millisecond/24h subtraction.
69
- function localDayNumber(d) {
70
- return Math.floor(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / 86_400_000);
71
- }
72
- // The date prefix shown before a message's HH:MM, by how many *local calendar
73
- // days* ago it was sent (relative to `now`):
74
- // - today (or future/clock-skew): no prefix
75
- // - 1 day ago: "yest"
76
- // - 2–6 days ago: short weekday ("mon".."sun"). This range can never collide
77
- // with today's weekday or with yesterday, so the day name is unambiguous.
78
- // - 7+ days ago: a date. "the same weekday last week" is 7 days ago, so it
79
- // lands here (a date, not a weekday) — exactly the ambiguity we want to
80
- // avoid. This-year dates render "mmm-dd"; other years render "yyyy-mm-dd".
81
- export function datePrefix(when, now) {
82
- const diff = localDayNumber(now) - localDayNumber(when);
83
- if (diff <= 0)
84
- return '';
85
- if (diff === 1)
86
- return 'yest';
87
- if (diff <= 6)
88
- return WEEKDAYS[when.getDay()];
89
- if (when.getFullYear() === now.getFullYear()) {
90
- return `${MONTHS[when.getMonth()]}-${pad2(when.getDate())}`;
91
- }
92
- return `${when.getFullYear()}-${pad2(when.getMonth() + 1)}-${pad2(when.getDate())}`;
93
- }
94
- // A message timestamp: 24h local HH:MM, with a relative date prefix once the
95
- // message is no longer from today. `now` is injectable so the live view can
96
- // re-derive every prefix when the local day rolls over.
97
- export function formatTimestamp(when, now = new Date()) {
98
- const d = typeof when === 'string' ? new Date(when) : when;
99
- const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
100
- const prefix = datePrefix(d, now);
101
- return prefix ? `${prefix} ${time}` : time;
102
- }
103
- export function formatLine(m, now = new Date()) {
104
- const lvl = m.senderLevel != null ? dim(` lv${m.senderLevel}`) : '';
105
- // Colour the author's handle in their belt/rank colour (see rankColor).
106
- // Strip control chars from the handle too, not just content: it's untrusted
107
- // (user-registered) and gets wrapped in ANSI colour codes before printing, so
108
- // an embedded escape sequence would otherwise hijack the viewer's terminal.
109
- const handle = rankColor(m.senderLevel, stripControl(m.senderHandle));
110
- return `${dim(formatTimestamp(m.createdAt, now))} ${handle}${lvl}: ${stripControl(m.content)}`;
111
- }
112
- // Optimistic "sending" line: the just-typed message rendered faded with a
113
- // status tag, shown the instant you hit enter and rewritten in place once the
114
- // server confirms it (or it fails). Handle/time are best-effort — we don't know
115
- // our own rank level locally, so the confirmed line from the server is what
116
- // applies the proper belt colour. `tag` is passed pre-styled by the caller
117
- // (dim for sending, red for failed).
118
- export function formatPendingLine(handle, content, tag, at = new Date().toISOString(), now = new Date()) {
119
- const ts = formatTimestamp(at, now);
120
- const base = `${ts} ${stripControl(handle)}: ${stripControl(content)}`;
121
- return `${dim(base)} ${tag}`;
122
- }
123
- // API returns newest-first; print oldest-first like a chat log.
124
- function renderMessages(messages) {
125
- if (messages.length === 0) {
126
- console.log(dim('(no messages yet)'));
127
- return;
128
- }
129
- for (const m of [...messages].reverse()) {
130
- console.log(formatLine(m));
131
- }
132
- }
133
- async function chatTail(json) {
134
- // Public endpoint — no auth required (the whole point of the cached tail), but
135
- // still resolve the backend the same way so --env and your logged-in env work.
136
- const session = await loadSession();
137
- const res = await fetch(`${baseUrl(session)}${CHANNEL_BASE}/tail`);
138
- if (!res.ok) {
139
- error(await readError(res));
140
- process.exit(1);
141
- }
142
- const data = (await res.json());
143
- if (json) {
144
- printJson(data);
145
- return;
146
- }
147
- renderMessages(data.messages);
148
- }
149
- async function chatPost(words, json) {
150
- const content = words.join(' ').trim();
151
- if (!content) {
152
- error('usage: hacklab chat post <message>');
153
- process.exit(1);
154
- }
155
- const session = await requireSession();
156
- const res = await fetch(`${baseUrl(session)}${CHANNEL_BASE}`, {
157
- method: 'POST',
158
- headers: {
159
- 'Content-Type': 'application/json',
160
- Authorization: `Bearer ${session.token}`,
161
- },
162
- body: JSON.stringify({ content }),
163
- });
164
- if (!res.ok) {
165
- error(await readError(res));
166
- process.exit(1);
167
- }
168
- const data = await res.json();
169
- if (json) {
170
- printJson(data);
171
- return;
172
- }
173
- success('posted.');
174
- await captureEvent(session.handle, 'cli_chat_message_posted', {
175
- content_length: content.length,
176
- });
177
- }
178
- async function chatHistory(args, json) {
179
- const before = flagValue(args, '--before');
180
- const session = await requireSession();
181
- const url = new URL(`${baseUrl(session)}${CHANNEL_BASE}/history`);
182
- if (before)
183
- url.searchParams.set('before', before);
184
- const res = await fetch(url, {
185
- headers: { Authorization: `Bearer ${session.token}` },
186
- });
187
- if (!res.ok) {
188
- error(await readError(res));
189
- process.exit(1);
190
- }
191
- const data = (await res.json());
192
- if (json) {
193
- printJson(data);
194
- return;
195
- }
196
- renderMessages(data.messages);
197
- if (data.nextCursor) {
198
- console.log(dim(`\nolder → hacklab chat history --before ${data.nextCursor}`));
199
- }
200
- }
201
- async function chatDms(json) {
202
- const session = await requireSession();
203
- const res = await fetch(`${baseUrl(session)}/api/chat/dms`, {
204
- headers: { Authorization: `Bearer ${session.token}` },
205
- });
206
- if (!res.ok) {
207
- error(await readError(res));
208
- process.exit(1);
209
- }
210
- const data = (await res.json());
211
- if (json) {
212
- printJson(data);
213
- return;
214
- }
215
- if (data.conversations.length === 0) {
216
- console.log(dim('(no conversations yet)'));
217
- return;
218
- }
219
- for (const c of data.conversations) {
220
- const unread = c.unread_count > 0 ? ` ${green(`(${c.unread_count} new)`)}` : '';
221
- // Both the partner handle and the last-message preview are untrusted user
222
- // input wrapped in ANSI before printing — strip control chars so a crafted
223
- // handle or DM body can't inject escape sequences into the viewer's terminal.
224
- const preview = c.last_message_content
225
- ? dim(` — ${stripControl(c.last_message_content)}`)
226
- : '';
227
- console.log(`${bold(`@${stripControl(c.partner_handle)}`)}${unread}${preview}`);
228
- }
229
- }
230
- async function chatDm(args, json) {
231
- const handle = args[0];
232
- if (!handle) {
233
- error('usage: hacklab chat dm <handle> [message]');
234
- process.exit(1);
235
- }
236
- const message = args.slice(1).join(' ').trim();
237
- const session = await requireSession();
238
- const base = `${baseUrl(session)}/api/chat/dm/${encodeURIComponent(handle)}`;
239
- if (message) {
240
- const res = await fetch(base, {
241
- method: 'POST',
242
- headers: {
243
- 'Content-Type': 'application/json',
244
- Authorization: `Bearer ${session.token}`,
245
- },
246
- body: JSON.stringify({ content: message }),
247
- });
248
- if (!res.ok) {
249
- error(await readError(res));
250
- process.exit(1);
251
- }
252
- const data = await res.json();
253
- if (json) {
254
- printJson(data);
255
- return;
256
- }
257
- success(`sent to @${handle}.`);
258
- await captureEvent(session.handle, 'cli_dm_sent', {
259
- content_length: message.length,
260
- });
261
- return;
262
- }
263
- const res = await fetch(base, {
264
- headers: { Authorization: `Bearer ${session.token}` },
265
- });
266
- if (!res.ok) {
267
- error(await readError(res));
268
- process.exit(1);
269
- }
270
- const data = (await res.json());
271
- if (json) {
272
- printJson(data);
273
- return;
274
- }
275
- renderMessages(data.messages);
276
- }
277
- async function chatFlag(args, json) {
278
- const targetId = args[0];
279
- const reason = args[1];
280
- if (!targetId || !reason) {
281
- error('usage: hacklab chat flag <messageId> <off_topic|personal_attack|spam|other>');
282
- process.exit(1);
283
- }
284
- const session = await requireSession();
285
- const res = await fetch(`${baseUrl(session)}/api/chat/flag`, {
286
- method: 'POST',
287
- headers: {
288
- 'Content-Type': 'application/json',
289
- Authorization: `Bearer ${session.token}`,
290
- },
291
- body: JSON.stringify({ targetId, reason }),
292
- });
293
- if (!res.ok) {
294
- error(await readError(res));
295
- process.exit(1);
296
- }
297
- const data = await res.json();
298
- if (json) {
299
- printJson(data);
300
- return;
301
- }
302
- success('flagged for review.');
303
- }
304
- /**
305
- * Decide what a typed line means. A single leading slash is a local command and
306
- * never posts. A DOUBLED leading slash is how you post a message that starts with
307
- * a slash: it posts (and displays) verbatim, both slashes intact. Nothing is
308
- * stripped, so the optimistic placeholder, the POST body, and the server echo all
309
- * agree (reconcile-by-content) — and the server can reject single-leading-slash
310
- * content (a stale client posting a command it doesn't know is local) without
311
- * catching the doubled form. Pure + exported so these semantics are unit-tested
312
- * without driving readline.
313
- */
314
- export function parseLine(input) {
315
- const content = input.trim();
316
- if (!content)
317
- return { kind: 'empty' };
318
- if (content.startsWith('//'))
319
- return { kind: 'message', content };
320
- if (content.startsWith('/')) {
321
- const [cmd, ...args] = content.slice(1).trim().split(/\s+/);
322
- return { kind: 'command', cmd: cmd ?? '', args };
323
- }
324
- return { kind: 'message', content };
325
- }
326
- // Interactive live channel: short-poll the public tail and print new messages
327
- // above a readline prompt; press enter to post. CLI-first design means this is a
328
- // thin loop over the same endpoints the agent subcommands use.
329
- async function chatLive() {
330
- const session = await requireSession();
331
- const base = baseUrl(session);
332
- const seen = new Set();
333
- let started = false;
334
- // We learn our own handle from the first confirmed send (or the session) and
335
- // use it to match incoming messages back to our optimistic placeholders.
336
- let myHandle = session.handle;
337
- // The local calendar day we last rendered against. When it changes (midnight,
338
- // or waking from sleep across midnight), every message's relative date prefix
339
- // may be stale, so we repaint the whole transcript.
340
- let dayKey = localDayNumber(new Date());
341
- const log = [];
342
- const rl = readline.createInterface({
343
- input: process.stdin,
344
- output: process.stdout,
345
- prompt: dim('> '),
346
- });
347
- function renderEntry(e, now) {
348
- if (e.kind === 'msg')
349
- return formatLine(e.m, now);
350
- if (e.kind === 'text')
351
- return e.text;
352
- const tag = e.failed ? red(`[failed: ${e.failed}]`) : dim('[sending]');
353
- return formatPendingLine(myHandle ?? 'you', e.content, tag, e.at, now);
354
- }
355
- // How many terminal rows above the prompt an entry sits (1 = directly above).
356
- // Derived from its index in `log`: the last entry is one row up, and every
357
- // later line pushes earlier ones further up. Assumes one row per entry, as the
358
- // rest of this view does.
359
- function rowsUpOf(e) {
360
- const i = log.indexOf(e);
361
- return i === -1 ? 0 : log.length - i;
362
- }
363
- // Append a new entry as a line above the prompt without clobbering whatever
364
- // the user is mid-typing: clear the prompt line, write the entry, then redraw
365
- // the prompt + the in-progress input buffer.
366
- function appendEntry(e) {
367
- log.push(e);
368
- const inProgress = rl.line;
369
- readline.cursorTo(process.stdout, 0);
370
- readline.clearLine(process.stdout, 0);
371
- process.stdout.write(`${renderEntry(e, new Date())}\n`);
372
- process.stdout.write(rl.getPrompt() + inProgress);
373
- }
374
- // Append a local one-row line (slash-command output). Goes through appendEntry
375
- // as a `text` entry, so it counts as exactly one log row.
376
- function appendText(text) {
377
- appendEntry({ kind: 'text', text });
378
- }
379
- const SLASH_HELP = [
380
- dim('/who <handle> look up a hacker'),
381
- dim('/help this list'),
382
- dim('//<text> post a message that starts with a slash (shown as //)'),
383
- dim('(dm someone with `hacklab chat dm <handle> <message>`)'),
384
- ];
385
- // Dispatch a `/command`. Runs locally, never posts. Async commands (/who)
386
- // append their rows when the fetch lands — the prompt was already restored by
387
- // the caller, so the user keeps typing while the lookup is in flight.
388
- async function handleSlash(cmd, args) {
389
- if (cmd === 'who')
390
- return whoLookup(args[0]);
391
- if (cmd === 'help') {
392
- for (const line of SLASH_HELP)
393
- appendText(line);
394
- return;
395
- }
396
- appendText(dim(`unknown command: /${stripControl(cmd)}. try /help`));
397
- }
398
- // `/who <handle>` — the compact card, appended one row per line. Prints
399
- // nothing while the fetch is in flight (no placeholder to rewrite).
400
- async function whoLookup(rawHandle) {
401
- const handle = rawHandle?.replace(/^@/, '');
402
- if (!handle) {
403
- appendText(dim('usage: /who <handle>'));
404
- return;
405
- }
406
- const safe = stripControl(handle);
407
- try {
408
- const res = await fetch(`${base}/api/hackers/${encodeURIComponent(handle)}?src=chat`, { headers: { Authorization: `Bearer ${session.token}` } });
409
- if (res.status === 404) {
410
- appendText(dim(`no hacker named "${safe}"`));
411
- return;
412
- }
413
- if (!res.ok) {
414
- appendText(dim(`could not look up ${safe} (${res.status})`));
415
- return;
416
- }
417
- const data = (await res.json());
418
- if (!data.hacker) {
419
- appendText(dim(`no hacker named "${safe}"`));
420
- return;
421
- }
422
- for (const line of renderCompact(data.hacker))
423
- appendText(line);
424
- }
425
- catch {
426
- appendText(dim('could not reach hacklab'));
427
- }
428
- }
429
- // Rewrite an entry's existing line in place (it must still be on screen), then
430
- // restore the prompt + in-progress input. Used to swap an optimistic
431
- // placeholder for the confirmed (or failed) message with no second line.
432
- function rewriteEntry(e) {
433
- const rowsUp = rowsUpOf(e);
434
- if (rowsUp < 1)
435
- return;
436
- const inProgress = rl.line;
437
- readline.moveCursor(process.stdout, 0, -rowsUp);
438
- readline.cursorTo(process.stdout, 0);
439
- readline.clearLine(process.stdout, 0);
440
- process.stdout.write(renderEntry(e, new Date()));
441
- readline.cursorTo(process.stdout, 0);
442
- readline.moveCursor(process.stdout, 0, rowsUp);
443
- readline.clearLine(process.stdout, 0);
444
- process.stdout.write(rl.getPrompt() + inProgress);
445
- }
446
- // Repaint the entire transcript with fresh date prefixes. Clears the screen
447
- // *and* scrollback (\x1b[3J) so the rolled-over timestamps don't leave a stale
448
- // duplicate above. Rare — at most once per local-day change.
449
- function repaint() {
450
- const now = new Date();
451
- const inProgress = rl.line;
452
- process.stdout.write('\x1b[3J\x1b[2J\x1b[H');
453
- for (const e of log)
454
- process.stdout.write(`${renderEntry(e, now)}\n`);
455
- process.stdout.write(rl.getPrompt() + inProgress);
456
- }
457
- // Render a confirmed message exactly once. If it matches one of our pending
458
- // optimistic sends (same author + content), swap that placeholder in place;
459
- // otherwise append it. Dedupes whether the message arrives via the POST
460
- // response or the poll, whichever wins the race.
461
- function commitMessage(m) {
462
- if (seen.has(m.id))
463
- return;
464
- seen.add(m.id);
465
- const idx = log.findIndex((e) => e.kind === 'pending' &&
466
- e.content === m.content &&
467
- (myHandle ? m.senderHandle === myHandle : true));
468
- if (idx !== -1) {
469
- log[idx] = { kind: 'msg', m };
470
- rewriteEntry(log[idx]);
471
- }
472
- else {
473
- appendEntry({ kind: 'msg', m });
474
- }
475
- }
476
- // A send failed: flip its placeholder to a red [failed] tag instead of leaving
477
- // it stuck on [sending]. No-op if the poll already confirmed it (the
478
- // placeholder object is no longer in `log`).
479
- function failPending(placeholder, reason) {
480
- if (placeholder.kind !== 'pending' || placeholder.failed)
481
- return;
482
- if (!log.includes(placeholder))
483
- return;
484
- placeholder.failed = reason;
485
- rewriteEntry(placeholder);
486
- }
487
- async function poll() {
488
- // Day-rollover check runs every tick, independent of the network, so the
489
- // relative prefixes advance even while the fetch is failing.
490
- const k = localDayNumber(new Date());
491
- if (started && k !== dayKey) {
492
- dayKey = k;
493
- repaint();
494
- }
495
- try {
496
- const res = await fetch(`${base}${CHANNEL_BASE}/tail`);
497
- if (!res.ok)
498
- return;
499
- const data = (await res.json());
500
- const ordered = [...data.messages].reverse();
501
- if (!started) {
502
- const now = new Date();
503
- for (const m of ordered) {
504
- seen.add(m.id);
505
- const e = { kind: 'msg', m };
506
- log.push(e);
507
- console.log(renderEntry(e, now));
508
- }
509
- started = true;
510
- rl.prompt();
511
- return;
512
- }
513
- for (const m of ordered)
514
- commitMessage(m);
515
- }
516
- catch {
517
- // Transient network error — keep polling silently.
518
- }
519
- }
520
- console.log(dim('hacklab channel — live. type a message + enter to post, ctrl-c to quit.'));
521
- await poll();
522
- const timer = setInterval(poll, 1500);
523
- rl.on('line', async (input) => {
524
- const intent = parseLine(input);
525
- if (intent.kind === 'empty') {
526
- rl.prompt();
527
- return;
528
- }
529
- // Replace readline's echo of the typed line ("> message") with a faded
530
- // optimistic line. Without this the echo plus the confirmed message read as
531
- // a duplicate. clearScreenDown also handles a long line that wrapped.
532
- const PROMPT_WIDTH = 2; // visible width of "> "
533
- const cols = process.stdout.columns ?? 80;
534
- const echoRows = Math.max(1, Math.ceil((PROMPT_WIDTH + input.length) / cols));
535
- readline.moveCursor(process.stdout, 0, -echoRows);
536
- readline.cursorTo(process.stdout, 0);
537
- readline.clearScreenDown(process.stdout);
538
- // A local command never posts. Restore the prompt first, then let the
539
- // (possibly async) command append its rows via appendEntry — one row per
540
- // entry, so rowsUpOf stays correct even mid optimistic-send.
541
- if (intent.kind === 'command') {
542
- rl.prompt();
543
- void handleSlash(intent.cmd, intent.args);
544
- return;
545
- }
546
- // parseLine already stripped the escaping slash from a `//` message, so the
547
- // placeholder and the POST body agree — the poll reconciles optimistic sends
548
- // by exact content match, and a mismatch sticks the placeholder on [sending].
549
- const postContent = intent.content;
550
- // Faded placeholder, one row above the freshly drawn prompt. Written inline
551
- // (not via appendEntry) so we can hand control back with rl.prompt(), which
552
- // keeps readline's cursor model correct for the next keystroke.
553
- const placeholder = {
554
- kind: 'pending',
555
- content: postContent,
556
- at: new Date().toISOString(),
557
- };
558
- log.push(placeholder);
559
- process.stdout.write(`${renderEntry(placeholder, new Date())}\n`);
560
- rl.prompt();
561
- try {
562
- const res = await fetch(`${base}${CHANNEL_BASE}`, {
563
- method: 'POST',
564
- headers: {
565
- 'Content-Type': 'application/json',
566
- Authorization: `Bearer ${session.token}`,
567
- },
568
- body: JSON.stringify({ content: postContent }),
569
- });
570
- if (!res.ok) {
571
- failPending(placeholder, await readError(res));
572
- return;
573
- }
574
- const data = (await res.json());
575
- const m = data.message;
576
- if (m?.id) {
577
- myHandle ??= m.senderHandle;
578
- commitMessage(m);
579
- }
580
- else {
581
- failPending(placeholder, 'no message returned');
582
- }
583
- }
584
- catch {
585
- failPending(placeholder, 'network error');
586
- }
587
- });
588
- rl.on('SIGINT', () => {
589
- clearInterval(timer);
590
- rl.close();
591
- process.stdout.write('\n');
592
- process.exit(0);
593
- });
594
- }
595
- function printChatHelp() {
596
- console.log(bold('hacklab chat'));
597
- console.log('');
598
- console.log(` ${dim('(no args)')} open the live channel (type to post)`);
599
- console.log(` ${dim('live')} open the live channel (same as no args)`);
600
- console.log(` ${dim('tail')} read the latest channel messages`);
601
- console.log(` ${dim('post <message>')} post to the channel`);
602
- console.log(` ${dim('history [--before C]')} older channel messages`);
603
- console.log(` ${dim('dms')} list your direct-message threads`);
604
- console.log(` ${dim('dm <handle> [msg]')} read a DM thread, or send if msg given`);
605
- console.log(` ${dim('flag <id> <reason>')} report a message to moderation`);
606
- console.log('');
607
- console.log(` ${dim('--json')} machine-readable output (non-interactive subcommands)`);
608
- }
609
- export async function chat(args) {
610
- const json = args.includes('--json');
611
- const rest = args.filter((a) => a !== '--json');
612
- const sub = rest[0];
613
- const subArgs = rest.slice(1);
614
- // Explicit help only — a bare `hacklab chat` is the human entry point and
615
- // drops you straight into the live channel. The tail/post/history/dms/dm/flag
616
- // subcommands stay for scripting and agents (pair them with --json).
617
- if (sub === 'help' || sub === '--help' || sub === '-h') {
618
- printChatHelp();
619
- return;
620
- }
621
- switch (sub) {
622
- case undefined:
623
- case 'live':
624
- return chatLive();
625
- case 'tail':
626
- return chatTail(json);
627
- case 'post':
628
- return chatPost(subArgs, json);
629
- case 'history':
630
- return chatHistory(subArgs, json);
631
- case 'dms':
632
- return chatDms(json);
633
- case 'dm':
634
- return chatDm(subArgs, json);
635
- case 'flag':
636
- return chatFlag(subArgs, json);
637
- default:
638
- printChatHelp();
639
- process.exit(1);
640
- }
641
- }
642
- //# sourceMappingURL=chat.js.map