@logicseek/hello-cc 0.1.5 → 0.1.6

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 (85) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/bin/hcc.mjs +1541 -3430
  3. package/docs/README.md +2 -0
  4. package/docs/README.zh-CN.md +3 -1
  5. package/docs/architecture-split-review.md +217 -0
  6. package/docs/architecture.md +385 -0
  7. package/docs/architecture.zh-CN.md +364 -0
  8. package/docs/layout-split-stack-audit.md +298 -0
  9. package/lib/automation.mjs +8 -0
  10. package/lib/cli-args.mjs +1 -1
  11. package/lib/cli-runtime.mjs +44 -0
  12. package/lib/coordination-state.mjs +218 -0
  13. package/lib/core/coordination/automation.mjs +222 -0
  14. package/lib/core/coordination/locks.mjs +46 -0
  15. package/lib/core/coordination/messages.mjs +116 -0
  16. package/lib/core/coordination/tasks.mjs +224 -0
  17. package/lib/core/coordination/teams.mjs +73 -0
  18. package/lib/core/coordination/timeline.mjs +187 -0
  19. package/lib/core/peers/bindings.mjs +83 -0
  20. package/lib/core/peers/format.mjs +13 -0
  21. package/lib/core/peers/identity.mjs +53 -0
  22. package/lib/core/peers/liveness.mjs +90 -0
  23. package/lib/core/peers/reconcile.mjs +238 -0
  24. package/lib/core/peers/session.mjs +14 -0
  25. package/lib/core/sessions/launch.mjs +89 -0
  26. package/lib/db/schema.mjs +324 -0
  27. package/lib/db/stores/peers.mjs +231 -0
  28. package/lib/db-schema.mjs +7 -324
  29. package/lib/errors.mjs +1 -7
  30. package/lib/handoff.mjs +26 -0
  31. package/lib/help.mjs +3 -0
  32. package/lib/integrations/hooks.mjs +171 -0
  33. package/lib/integrations/peers/identity.mjs +208 -0
  34. package/lib/integrations/peers/processes.mjs +151 -0
  35. package/lib/integrations/providers.mjs +282 -0
  36. package/lib/integrations/shims/script.mjs +384 -0
  37. package/lib/integrations/shims.mjs +106 -0
  38. package/lib/json-file.mjs +1 -14
  39. package/lib/locks.mjs +10 -0
  40. package/lib/messages.mjs +3 -0
  41. package/lib/package-meta.mjs +6 -24
  42. package/lib/peer-bindings.mjs +14 -0
  43. package/lib/peer-format.mjs +4 -0
  44. package/lib/peer-identity.mjs +15 -0
  45. package/lib/project-context.mjs +6 -0
  46. package/lib/project-registry.mjs +7 -0
  47. package/lib/provider-commands.mjs +13 -0
  48. package/lib/release/package-meta.mjs +31 -0
  49. package/lib/release/release-notes.mjs +40 -0
  50. package/lib/release-notes.mjs +6 -40
  51. package/lib/runtime/client.mjs +42 -0
  52. package/lib/runtime/paths.mjs +45 -0
  53. package/lib/runtime/project-context.mjs +32 -0
  54. package/lib/runtime/projects.mjs +76 -0
  55. package/lib/runtime/state.mjs +128 -0
  56. package/lib/runtime-client.mjs +3 -0
  57. package/lib/runtime-paths.mjs +11 -45
  58. package/lib/runtime-state.mjs +11 -0
  59. package/lib/session-launch.mjs +17 -0
  60. package/lib/setup.mjs +17 -293
  61. package/lib/shared/errors.mjs +7 -0
  62. package/lib/shared/json-file.mjs +14 -0
  63. package/lib/shim-script.mjs +1 -384
  64. package/lib/state-render.mjs +5 -0
  65. package/lib/task-cli.mjs +45 -0
  66. package/lib/task-liveness.mjs +8 -0
  67. package/lib/task-store.mjs +3 -0
  68. package/lib/team-planning.mjs +7 -0
  69. package/lib/terminal/tmux.mjs +372 -0
  70. package/lib/timeline.mjs +12 -0
  71. package/lib/tmux.mjs +18 -0
  72. package/lib/ui/help.mjs +419 -0
  73. package/lib/ui/state-render.mjs +61 -0
  74. package/lib/web/http.mjs +55 -0
  75. package/lib/web/peer-actions.mjs +270 -0
  76. package/lib/web/runtime.mjs +154 -0
  77. package/lib/web/ui-template.mjs +2055 -0
  78. package/lib/web-http.mjs +7 -55
  79. package/lib/web-peer-actions.mjs +3 -0
  80. package/lib/web-runtime.mjs +17 -108
  81. package/lib/web-ui-template.mjs +3 -1909
  82. package/package.json +1 -1
  83. package/scripts/github-release.mjs +2 -2
  84. package/scripts/regression.mjs +2468 -17
  85. package/scripts/release-notes.mjs +2 -2
@@ -0,0 +1,419 @@
1
+ export function createHelpFunctions(config = {}) {
2
+ const productName = config.productName || 'hello-cc';
3
+ const version = config.version || '';
4
+ const cliName = config.cliName || 'hcc';
5
+ const npmPackageName = config.npmPackageName || '@logicseek/hello-cc';
6
+
7
+ return {
8
+ helpMain() {
9
+ console.log(`${productName} ${version}
10
+
11
+ Project-local coordination bus for multiple Claude Code and Codex CLI sessions.
12
+
13
+ Usage:
14
+ ${cliName} [--root DIR] [--db FILE] [--json] <command> [args]
15
+
16
+ Commands:
17
+ web Start coordination, shims, tmux, and browser console
18
+ up Initialize local coordination only
19
+ down Stop the running hello-cc web runtime
20
+ update Update the global npm install of hello-cc
21
+ uninstall Remove hooks, shims, and optional project data
22
+ init Initialize .hello-cc/mesh.db and guidance
23
+ register --peer ID Register or update a peer session identity
24
+ join --peer ID Register this shell and print eval-able env
25
+ env --peer ID Print eval-able HCC_PEER/HCC_ROOT/HCC_DB exports
26
+ heartbeat [--peer ID] Mark the current peer alive, optionally renew locks
27
+ peers List known peers
28
+ status [--peer ID] Show project coordination state
29
+ state [--peer ID] Show timeline and next coordination action
30
+ scan [--register] Detect existing Claude/Codex sessions
31
+ prompt --peer ID Print copy/paste session instructions
32
+ run --peer ID -- COMMAND Register a peer and run a command in this terminal
33
+ peer <subcommand> Start, attach, list, and stop tmux-backed peers
34
+ tmux gc [--yes] Clean stale DB-proven hcc-managed tmux sessions
35
+ inject PEER TEXT Write text into an attached terminal
36
+ ask PEER MESSAGE Send a direct work request to one peer
37
+ broadcast MESSAGE Send a work request to all peers
38
+ task <subcommand> Create, list, claim, update, finish tasks
39
+ team <subcommand> Plan, start, and inspect explicit task teams
40
+ msg <subcommand> Send, read, and ack messages
41
+ lock <subcommand> Acquire, renew, release, and list advisory locks
42
+ handoff <subcommand> Create and list handoffs
43
+ event tail Show recent coordination events
44
+ gc [--older-than DAYS] [--yes] Clean up stale peers, events, tasks, and buf files
45
+ Internal:
46
+ hook Hook entrypoint used by Claude/Codex
47
+ find-root Shim helper
48
+ which-real Shim helper
49
+
50
+ Environment:
51
+ HCC_ROOT Override project root
52
+ HCC_DB Override database path
53
+ HCC_PEER Default peer id; inferred automatically when absent
54
+ HCC_WEB_TOKEN Replace and save the stable web access token
55
+ `);
56
+ },
57
+
58
+ helpTask() {
59
+ console.log(`${cliName} task
60
+
61
+ Usage:
62
+ ${cliName} task create --title TEXT [--body TEXT] [--from ID] [--to ID] [--priority N]
63
+ ${cliName} task create --title TEXT --parent N [--team-role ROLE]
64
+ ${cliName} task list [--status pending|claimed|running|review|blocked|done|abandoned] [--peer ID] [--all]
65
+ ${cliName} task claim [--peer ID] --id N[,N] [--id N] [--ids N,N] [--force]
66
+ ${cliName} task takeover [--peer ID] --id N --reason TEXT [--policy any|blocked|stale|blocked-or-stale] [--stale-after SECONDS]
67
+ ${cliName} task next [--peer ID] [--force] [--count N]
68
+ ${cliName} task update [--peer ID] --id N --status STATUS [--summary TEXT] [--body TEXT] [--to ID]
69
+ ${cliName} task done [--peer ID] --id N --summary TEXT
70
+
71
+ Default task list shows all project tasks that are not done or abandoned.
72
+ --peer is an explicit filter; HCC_PEER does not hide other open tasks.
73
+ Messages use per-peer unread ack state; tasks do not.
74
+ task next returns your existing claimed/running/review/blocked task before
75
+ claiming another pending task. Use --force when intentionally taking additional
76
+ pending tasks; combine it with --count N for explicit batch claims.
77
+ Use task takeover when explicitly taking over a non-complete task from another
78
+ owner; it records the previous owner, requires a reason, and notifies them.
79
+ Use --policy blocked, stale, or blocked-or-stale to require an auditable
80
+ precondition before takeover. The default policy is any for backward
81
+ compatibility.
82
+
83
+ If --peer/--from and HCC_PEER are absent, hcc auto-joins the current terminal
84
+ as a stable project-local peer.
85
+ `);
86
+ },
87
+
88
+ helpTeam() {
89
+ console.log(`${cliName} team
90
+
91
+ Usage:
92
+ ${cliName} team plan --from-task N [--item ROLE:TITLE] [--item PEER:ROLE:TITLE] [--workers A,B|codex:2,claude:1]
93
+ ${cliName} team start --from-task N [--item ROLE:TITLE] [--item PEER:ROLE:TITLE] [--workers A,B|codex:2,claude:1] [--force]
94
+ ${cliName} team status --task N
95
+
96
+ team plan is read-only. team start creates explicit child tasks under the parent
97
+ task and optionally assigns them to workers. It does not silently spawn model
98
+ processes or override the current-task rule.
99
+ workers may be explicit peer ids or kind counts such as codex:2,claude:1.
100
+ `);
101
+ },
102
+
103
+ helpState() {
104
+ console.log(`${cliName} state
105
+
106
+ Usage:
107
+ ${cliName} state [--peer ID] [--resource PATH] [--scope SCOPE] [--intent read|review|work|write|stop|finish]
108
+
109
+ Shows the current collaboration timeline plus a machine-readable coordination
110
+ state machine. With --json, the response includes automation.next_action.argv so
111
+ agents can execute the suggested hcc command explicitly and leave an audit trail.
112
+ automation.current_task shows the peer's active claimed/running/review/blocked
113
+ task when one exists.
114
+ If a current task looks splittable, automation may suggest hcc team plan; this
115
+ is read-only until hcc team start is run explicitly.
116
+ With --intent read or --intent review, state treats resources as snapshot
117
+ inspection and does not suggest acquiring file locks. With write/work intents,
118
+ --scope lets agents coordinate independent regions of the same resource.
119
+
120
+ State does not execute coordination actions: it does not ack messages, claim
121
+ tasks, acquire locks, send messages, create handoffs, or mark tasks done. Opening
122
+ state may still perform normal SQLite schema maintenance for known project DBs.
123
+ `);
124
+ },
125
+
126
+ helpJoin() {
127
+ console.log(`${cliName} join
128
+
129
+ Usage:
130
+ eval "$(${cliName} join --peer ID [--kind codex|claude|shell|other] [--role ROLE])"
131
+
132
+ Examples:
133
+ eval "$(${cliName} join --peer codex-current --kind codex)"
134
+ ${cliName} status
135
+
136
+ This registers the current shell as a peer and prints shell exports for
137
+ HCC_PEER, HCC_ROOT, and HCC_DB. A child CLI cannot mutate its parent shell
138
+ environment directly, so use eval to apply the exports to the current window.
139
+ `);
140
+ },
141
+
142
+ helpEnv() {
143
+ console.log(`${cliName} env
144
+
145
+ Usage:
146
+ eval "$(${cliName} env --peer ID)"
147
+
148
+ Examples:
149
+ eval "$(${cliName} env --peer codex-current)"
150
+
151
+ This only prints shell exports. Use hcc join when you also want to register
152
+ the peer in the project bus.
153
+ `);
154
+ },
155
+
156
+ helpMsg() {
157
+ console.log(`${cliName} msg
158
+
159
+ Usage:
160
+ ${cliName} msg send [--from ID] [--to ID|all] --body TEXT [--task N] [--kind note|task|handoff]
161
+ ${cliName} msg inbox [--peer ID] [--wait SEC] [--all] [--limit N]
162
+ ${cliName} msg ack [--peer ID] --id N
163
+ ${cliName} msg reply [--from ID] --id N --body TEXT [--to ID] [--kind reply]
164
+ ${cliName} msg thread --id N [--limit N]
165
+
166
+ If --peer/--from and HCC_PEER are absent, hcc auto-joins the current terminal.
167
+ Use msg reply when answering a message so the response stays in the same thread.
168
+ `);
169
+ },
170
+
171
+ helpAsk() {
172
+ console.log(`${cliName} ask
173
+
174
+ Usage:
175
+ ${cliName} ask PEER MESSAGE [--from ID] [--task N] [--inject]
176
+ ${cliName} ask --to PEER --body TEXT [--from ID] [--task N] [--inject]
177
+
178
+ Examples:
179
+ ${cliName} ask claude-a "Please review task #3."
180
+ ${cliName} ask --to codex-b --body "Can you run the router tests?" --task 3
181
+ ${cliName} ask claude-a "Please review task #3." --inject
182
+ `);
183
+ },
184
+
185
+ helpBroadcast() {
186
+ console.log(`${cliName} broadcast
187
+
188
+ Usage:
189
+ ${cliName} broadcast MESSAGE [--from ID] [--task N] [--inject]
190
+ ${cliName} broadcast --body TEXT [--from ID] [--task N] [--inject]
191
+
192
+ Example:
193
+ ${cliName} broadcast "Pause edits under src/router until lock clears."
194
+ `);
195
+ },
196
+
197
+ helpInject() {
198
+ console.log(`${cliName} inject
199
+
200
+ Usage:
201
+ ${cliName} inject PEER TEXT [--no-enter]
202
+ ${cliName} inject --peer PEER --body TEXT [--no-enter]
203
+
204
+ Examples:
205
+ ${cliName} inject codex-a "hcc msg inbox --peer codex-a"
206
+ ${cliName} inject claude-a "Please review task #3."
207
+
208
+ This works for peers attached to the running hcc web runtime, including
209
+ tmux-backed local terminals, attached tmux panes, and shim-launched tmux
210
+ terminals.
211
+ `);
212
+ },
213
+
214
+ helpPeer() {
215
+ console.log(`${cliName} peer
216
+
217
+ Usage:
218
+ ${cliName} peer list
219
+ ${cliName} peer start PEER [--kind codex|claude|shell] [--role ROLE] [--cwd DIR] [--resume ID|NAME]
220
+ ${cliName} peer start PEER --kind codex --last
221
+ ${cliName} peer start PEER --kind claude --continue
222
+ ${cliName} peer start PEER [--kind codex|claude|shell] [--role ROLE] [--cwd DIR] -- COMMAND [ARGS...]
223
+ ${cliName} peer attach PEER [--pane PANE] [--kind codex|claude|shell] [--role ROLE] [--cwd DIR]
224
+ ${cliName} peer stop PEER
225
+
226
+ Examples:
227
+ ${cliName} peer start codex-a --kind codex -- codex
228
+ ${cliName} peer start codex-a --kind codex --resume 00000000-0000-0000-0000-000000000000
229
+ ${cliName} peer start codex-a --kind codex --last
230
+ ${cliName} peer start claude-a --kind claude -- claude
231
+ ${cliName} peer start claude-a --kind claude --resume 00000000-0000-0000-0000-000000000000
232
+ ${cliName} peer start claude-a --kind claude --continue
233
+ ${cliName} peer attach codex-a --pane %1
234
+ ${cliName} peer stop codex-a
235
+
236
+ Start hcc web first. peer start creates a local tmux-backed terminal by default.
237
+ The web runtime attaches to that terminal; hcc down stops only the web runtime
238
+ and leaves the tmux terminal alive. peer attach imports an existing tmux pane.
239
+ If --pane is omitted, peer attach uses the current tmux pane when available.
240
+ Use --force only when intentionally overriding a provider-session or pane binding.
241
+ `);
242
+ },
243
+
244
+ helpTmux() {
245
+ console.log(`${cliName} tmux
246
+
247
+ Usage:
248
+ ${cliName} tmux gc [--peer ID] [--older-than DAYS] [--dry-run] [--yes]
249
+
250
+ Examples:
251
+ ${cliName} tmux gc
252
+ ${cliName} tmux gc --older-than 14 --yes
253
+
254
+ tmux gc only considers sessions that are proven by this project's database:
255
+ peer_bindings.transport must be tmux, runtime_target must point at a live tmux
256
+ pane, and the actual tmux session name must match hello-cc's managed session
257
+ name for that peer and project root. It can also remove old sessions recorded by
258
+ tmux.session.rebind_cleanup_failed events after a successful provider rebind.
259
+
260
+ It skips sessions with attached tmux clients, sessions still managed by the
261
+ running Web runtime, sessions with a different HCC_ROOT tmux marker,
262
+ non-matching session names, and sessions newer than --older-than. The default is
263
+ a dry-run; deletion requires --yes.
264
+ `);
265
+ },
266
+
267
+ helpGc() {
268
+ console.log(`${cliName} gc
269
+
270
+ Usage:
271
+ ${cliName} gc [--older-than DAYS] [--yes]
272
+
273
+ Examples:
274
+ ${cliName} gc
275
+ ${cliName} gc --older-than 14 --yes
276
+
277
+ Clean stale coordination database records and old buffer files for the current
278
+ project. By default this is a dry-run; deletion requires --yes.
279
+ `);
280
+ },
281
+
282
+ helpLock() {
283
+ console.log(`${cliName} lock
284
+
285
+ Usage:
286
+ ${cliName} lock acquire [--peer ID] --resource PATH [--scope SCOPE] [--task N] [--ttl SEC] [--reason TEXT]
287
+ ${cliName} lock renew [--peer ID] --resource PATH [--scope SCOPE] [--ttl SEC]
288
+ ${cliName} lock release [--peer ID] --resource PATH [--scope SCOPE] [--force]
289
+ ${cliName} lock list [--all]
290
+
291
+ Omit --scope to lock the whole resource. Different scopes on the same resource
292
+ can be held concurrently, but a whole-resource lock conflicts with every scope.
293
+ `);
294
+ },
295
+
296
+ helpHandoff() {
297
+ console.log(`${cliName} handoff
298
+
299
+ Usage:
300
+ ${cliName} handoff create [--from ID] --summary TEXT [--task N] [--to ID] [--changed-files JSON_OR_CSV] [--tests TEXT] [--risks TEXT]
301
+ ${cliName} handoff list [--task N] [--limit N]
302
+ `);
303
+ },
304
+
305
+ helpEvent() {
306
+ console.log(`${cliName} event
307
+
308
+ Usage:
309
+ ${cliName} event tail [--limit N]
310
+ `);
311
+ },
312
+
313
+ helpRun() {
314
+ console.log(`${cliName} run
315
+
316
+ Usage:
317
+ ${cliName} run --peer ID --kind codex|claude|shell --role ROLE -- COMMAND [ARGS...]
318
+
319
+ Examples:
320
+ ${cliName} run --peer codex-a --kind codex --role peer -- codex
321
+ ${cliName} run --peer claude-a --kind claude --role peer -- claude
322
+
323
+ This keeps the CLI in your current terminal while injecting HCC_PEER,
324
+ HCC_ROOT, and HCC_DB so the session can use the shared bus.
325
+ Use hcc peer start, hcc web, or the installed claude/codex shims when the
326
+ session should also be browser-controllable.
327
+ `);
328
+ },
329
+
330
+ helpUp() {
331
+ console.log(`${cliName} up
332
+
333
+ Usage:
334
+ ${cliName} up [--no-discover] [--no-guidance]
335
+
336
+ Examples:
337
+ ${cliName} up
338
+
339
+ This initializes the project-local coordination bus, writes bounded guidance,
340
+ installs Claude/Codex hooks when missing, and registers currently detected
341
+ sessions. It does not start the browser terminal console.
342
+
343
+ Run hcc web for the default full experience: local coordination, hooks, shims,
344
+ tmux-backed terminal sessions, and browser control.
345
+ `);
346
+ },
347
+
348
+ helpDown() {
349
+ console.log(`${cliName} down
350
+
351
+ Usage:
352
+ ${cliName} down
353
+
354
+ Stops the web runtime started by hcc web for this project.
355
+ `);
356
+ },
357
+
358
+ helpUpdate() {
359
+ console.log(`${cliName} update
360
+
361
+ Usage:
362
+ ${cliName} update [--tag TAG] [--registry URL] [--dry-run]
363
+
364
+ Examples:
365
+ ${cliName} update
366
+ ${cliName} update --tag latest
367
+ ${cliName} update --dry-run
368
+
369
+ Updates the global npm install by running:
370
+ npm install -g ${npmPackageName}@TAG
371
+
372
+ The default TAG is latest.
373
+ `);
374
+ },
375
+
376
+ helpUninstall() {
377
+ console.log(`${cliName} uninstall
378
+
379
+ Usage:
380
+ ${cliName} uninstall [--purge --yes]
381
+
382
+ Stops the current project runtime and removes user-level hello-cc integrations:
383
+ Claude/Codex hooks and claude/codex shims.
384
+
385
+ With --purge --yes, also removes current project data:
386
+ .hello-cc/
387
+ hello-cc blocks from CLAUDE.md and AGENTS.md
388
+ `);
389
+ },
390
+
391
+ helpWeb() {
392
+ console.log(`${cliName} web
393
+
394
+ Usage:
395
+ ${cliName} web [--host HOST] [--port N] [--token TEXT] [--local] [--no-token] [--no-discover] [--no-guidance]
396
+
397
+ Examples:
398
+ ${cliName} web
399
+ HCC_WEB_TOKEN='long-token' ${cliName} web --port 8787
400
+ ${cliName} web --local
401
+
402
+ This is the default one-command entrypoint. It prepares local coordination,
403
+ installs Claude/Codex hooks and shims, ensures tmux is available, starts the
404
+ browser terminal console as a background runtime, prints the URL, PID, runtime
405
+ file, and log file, then returns the terminal to you.
406
+
407
+ By default, the web runtime listens on 0.0.0.0 and uses a saved token,
408
+ generating one on first use. Use HCC_WEB_TOKEN or --token to replace the saved
409
+ token, --local to bind only to 127.0.0.1, or --no-token only for a trusted
410
+ local/test environment.
411
+
412
+ After hcc web, plain claude/codex commands started from a new shell are wrapped
413
+ as local tmux-backed terminals. Existing ordinary terminals can communicate
414
+ through the bus, but cannot be visually attached unless they were started under
415
+ tmux/screen or a hello-cc shim.
416
+ `);
417
+ }
418
+ };
419
+ }
@@ -0,0 +1,61 @@
1
+ import { table } from '../format.mjs';
2
+ import { uniqueList } from '../core/coordination/timeline.mjs';
3
+
4
+ function iso(ts) {
5
+ if (!ts) return '';
6
+ return new Date(ts * 1000).toISOString().replace(/\.\d{3}Z$/, '.000Z');
7
+ }
8
+
9
+ export function renderStatusSummary(s, peer = null) {
10
+ const taskSummary = s.tasks.length ? s.tasks.map((r) => `${r.status}:${r.n}`).join(', ') : 'none';
11
+ return [
12
+ `root: ${s.root}`,
13
+ `db: ${s.db}`,
14
+ `peers: active=${s.active_peers}, stale=${s.stale_peers}`,
15
+ `tasks: ${taskSummary}`,
16
+ `locks: active=${s.active_locks}`,
17
+ peer ? `inbox(${peer}): unread=${s.unread}` : null,
18
+ '',
19
+ 'recent events:',
20
+ table(s.recent_events, [
21
+ { label: 'id', value: (r) => `#${r.id}` },
22
+ { label: 'type', value: (r) => r.type },
23
+ { label: 'actor', value: (r) => r.actor || '' },
24
+ { label: 'task', value: (r) => r.task_id ? `#${r.task_id}` : '' },
25
+ { label: 'time', value: (r) => iso(r.created_at) }
26
+ ])
27
+ ].filter((line) => line !== null).join('\n');
28
+ }
29
+
30
+ export function normalizeStateResources(values) {
31
+ const list = Array.isArray(values) ? values : [values];
32
+ return uniqueList(list.flatMap((value) => String(value || '').split(',').map((part) => part.trim())));
33
+ }
34
+
35
+ export function renderStateSummary(data, peer, opts = {}) {
36
+ const timelineLimit = Number(opts.timelineLimit || 8);
37
+ const automation = data.automation;
38
+ const lines = [
39
+ `root: ${data.root}`,
40
+ `peer: ${peer}`,
41
+ automation.current_task ? `current task: #${automation.current_task.id} ${automation.current_task.status} ${automation.current_task.title}` : null,
42
+ `phase: ${automation.phase}`,
43
+ `next: ${automation.next_action.command || automation.next_action.kind}`,
44
+ `why: ${automation.next_action.reason}`
45
+ ].filter(Boolean);
46
+ if (automation.finish_actions.length) {
47
+ lines.push('', 'finish actions:');
48
+ lines.push(...automation.finish_actions.map((action) => `- ${action.command}`));
49
+ }
50
+ if (automation.warnings.length) {
51
+ lines.push('', 'warnings:');
52
+ lines.push(...automation.warnings.map((warning) => `- ${warning}`));
53
+ }
54
+ if (data.timeline.length) {
55
+ lines.push('', 'timeline:');
56
+ for (const item of data.timeline.slice(-timelineLimit)) {
57
+ lines.push(`- ${iso(item.ts)} ${item.source}:${item.source_id} ${item.title}${item.text ? ` — ${item.text}` : ''}`);
58
+ }
59
+ }
60
+ return lines.join('\n');
61
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import { CliError } from '../shared/errors.mjs';
3
+
4
+ function readRequestBody(req, limit = 1024 * 1024) {
5
+ return new Promise((resolve, reject) => {
6
+ let body = '';
7
+ req.on('data', (chunk) => {
8
+ body += chunk;
9
+ if (body.length > limit) {
10
+ reject(new CliError('REQUEST_TOO_LARGE', 'Request body is too large'));
11
+ req.destroy();
12
+ }
13
+ });
14
+ req.on('end', () => resolve(body));
15
+ req.on('error', reject);
16
+ });
17
+ }
18
+
19
+ export async function readJsonRequest(req) {
20
+ const body = await readRequestBody(req);
21
+ if (!body.trim()) return {};
22
+ return JSON.parse(body);
23
+ }
24
+
25
+ export function sendHttp(res, status, contentType, body) {
26
+ res.writeHead(status, {
27
+ 'Content-Type': contentType,
28
+ 'Cache-Control': 'no-store'
29
+ });
30
+ res.end(body);
31
+ }
32
+
33
+ export function sendJson(res, status, body) {
34
+ sendHttp(res, status, 'application/json; charset=utf-8', JSON.stringify(body, null, 2));
35
+ }
36
+
37
+ export function sendFile(res, filePath, contentType) {
38
+ try {
39
+ const body = fs.readFileSync(filePath);
40
+ res.writeHead(200, {
41
+ 'Content-Type': contentType,
42
+ 'Cache-Control': 'no-store'
43
+ });
44
+ res.end(body);
45
+ } catch {
46
+ sendJson(res, 404, { ok: false, error: { code: 'NOT_FOUND', message: 'Asset not found' } });
47
+ }
48
+ }
49
+
50
+ export function authOk(url, req, token) {
51
+ if (!token) return true;
52
+ const queryToken = url.searchParams.get('token');
53
+ const auth = req.headers.authorization || '';
54
+ return queryToken === token || auth === `Bearer ${token}`;
55
+ }