@astrosheep/square 0.3.33 → 0.3.35

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 (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +10 -10
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity-feed.js +1 -1
  5. package/dist/activity.d.ts +1 -0
  6. package/dist/activity.js +3 -2
  7. package/dist/artifact.js +2 -1
  8. package/dist/automatic-session.js +17 -1
  9. package/dist/boundary-presentation.d.ts +1 -0
  10. package/dist/boundary-presentation.js +16 -1
  11. package/dist/catch-decisions.d.ts +4 -0
  12. package/dist/catch-decisions.js +23 -7
  13. package/dist/cli/context.d.ts +1 -0
  14. package/dist/cli/context.js +26 -1
  15. package/dist/cli/observation-commands.d.ts +7 -1
  16. package/dist/cli/observation-commands.js +149 -63
  17. package/dist/cli/program.js +8 -2
  18. package/dist/cli/square-commands.d.ts +3 -1
  19. package/dist/cli/square-commands.js +42 -16
  20. package/dist/codex-queue.d.ts +1 -0
  21. package/dist/codex-queue.js +1 -1
  22. package/dist/decisions.d.ts +1 -0
  23. package/dist/decisions.js +29 -14
  24. package/dist/delivery-health.d.ts +1 -0
  25. package/dist/delivery-health.js +19 -6
  26. package/dist/delivery-operations.js +5 -5
  27. package/dist/delivery.d.ts +2 -0
  28. package/dist/delivery.js +1 -0
  29. package/dist/harness-links.js +15 -3
  30. package/dist/harness.js +3 -1
  31. package/dist/help.js +25 -16
  32. package/dist/inbox.d.ts +2 -0
  33. package/dist/inbox.js +4 -2
  34. package/dist/list.js +77 -24
  35. package/dist/model.d.ts +1 -3
  36. package/dist/paseo-connection.js +9 -3
  37. package/dist/paseo-state.d.ts +4 -1
  38. package/dist/paseo-state.js +2 -2
  39. package/dist/presentation.d.ts +4 -0
  40. package/dist/presentation.js +41 -12
  41. package/dist/runtime.d.ts +1 -0
  42. package/dist/square-actions.d.ts +5 -0
  43. package/dist/square-actions.js +31 -7
  44. package/dist/square-core.d.ts +11 -1
  45. package/dist/square-core.js +12 -9
  46. package/dist/square-facade.d.ts +5 -2
  47. package/dist/square-file-adapter.d.ts +1 -1
  48. package/dist/square-file-adapter.js +11 -4
  49. package/dist/square-wiring.d.ts +1 -0
  50. package/dist/square-wiring.js +5 -1
  51. package/dist/stream.d.ts +8 -1
  52. package/dist/stream.js +17 -6
  53. package/dist/views.d.ts +11 -5
  54. package/dist/views.js +42 -16
  55. package/dist/wake-sink.d.ts +2 -0
  56. package/dist/wake-sink.js +10 -1
  57. package/dist/watch.js +26 -9
  58. package/extensions/square-pi.js +41 -7
  59. package/package.json +1 -1
  60. package/skills/brainstorm/SKILL.md +8 -6
  61. package/skills/square/SKILL.md +10 -10
@@ -3,8 +3,8 @@ import { runCodexHookAsync } from '../codex-hook.js';
3
3
  import { sessionInbox } from '../inbox.js';
4
4
  import { sweepPendingNotifications } from '../notifications.js';
5
5
  import { cmdListSquares } from '../list.js';
6
- import { parseActivityId, sameName } from '../model.js';
7
- import { commandPrefix, participantCommandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, withPathOutput, quoteShell, } from '../presentation.js';
6
+ import { nameKey, parseActivityId, sameName } from '../model.js';
7
+ import { commandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
8
8
  import { actId, nowMs } from '../runtime.js';
9
9
  import { cmdStream, cmdStreamNdjson } from '../stream.js';
10
10
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
@@ -12,8 +12,23 @@ import { cmdWatch } from '../watch.js';
12
12
  import { openSquare } from '../square-file-adapter.js';
13
13
  import { closeOpenSquare } from '../open-square.js';
14
14
  import { historyPresentation, participantsPresentation, statusPresentation } from '../views.js';
15
- import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
15
+ import { fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
16
+ import { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from '../catch-decisions.js';
16
17
  const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
18
+ const HISTORY_DEFAULT_LIMIT = 10;
19
+ const HISTORY_MAX_LIMIT = 100;
20
+ const PARTICIPANTS_DEFAULT_LIMIT = 20;
21
+ const PARTICIPANTS_MAX_LIMIT = 100;
22
+ const INBOX_DEFAULT_LIMIT = 20;
23
+ const INBOX_MAX_LIMIT = 100;
24
+ const INBOX_DISPLAY_CHARS = 160;
25
+ function inboxDisplay(value) {
26
+ const truncated = truncateChars(value, INBOX_DISPLAY_CHARS - 1);
27
+ return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
28
+ }
29
+ function inboxLimitCommand(sessionId, json) {
30
+ return `square inbox --for-session ${quoteShell(sessionId)} --limit ${INBOX_MAX_LIMIT}${json ? ' --json' : ''}`;
31
+ }
17
32
  export const listCommand = {
18
33
  parse: (argv) => argv,
19
34
  async execute(argv, context) {
@@ -25,6 +40,9 @@ export const streamCommand = {
25
40
  parse(argv, context) {
26
41
  let ndjson = false;
27
42
  let forName;
43
+ let last = 10;
44
+ let after;
45
+ let hasLast = false;
28
46
  for (let index = 0; index < argv.length; index++) {
29
47
  if (argv[index] === '--ndjson')
30
48
  ndjson = true;
@@ -32,17 +50,32 @@ export const streamCommand = {
32
50
  forName = requireValue(argv, index, argv[index]);
33
51
  index += 1;
34
52
  }
53
+ else if (argv[index] === '--last') {
54
+ if (after !== undefined)
55
+ fail('Invalid stream options: --last and --after cannot be combined.');
56
+ last = parseNonNegativeInteger(requireValue(argv, index, argv[index]), '--last');
57
+ if (last > 100)
58
+ fail('Invalid --last: maximum is 100.');
59
+ hasLast = true;
60
+ index += 1;
61
+ }
62
+ else if (argv[index] === '--after') {
63
+ if (hasLast)
64
+ fail('Invalid stream options: --last and --after cannot be combined.');
65
+ after = parseActRef(requireValue(argv, index, argv[index]), '--after');
66
+ index += 1;
67
+ }
35
68
  else
36
69
  usage(context.command);
37
70
  }
38
- if (forName !== undefined && !ndjson)
71
+ if (!ndjson && argv.length > 0)
39
72
  usage(context.command);
40
- return { ndjson, forName };
73
+ return { ndjson, forName, start: after === undefined ? { kind: 'tail', last } : { kind: 'after', after } };
41
74
  },
42
75
  async execute(intent, context) {
43
76
  const squarePath = requireSquarePath(context);
44
77
  if (intent.ndjson)
45
- await cmdStreamNdjson(squarePath, intent.forName);
78
+ await cmdStreamNdjson(squarePath, intent.forName, intent.start);
46
79
  else
47
80
  await cmdStream(squarePath);
48
81
  },
@@ -53,6 +86,7 @@ export const catchCommand = {
53
86
  const name = requireParticipant(context.name);
54
87
  let idleMs;
55
88
  let mention;
89
+ let limit = CATCH_DEFAULT_LIMIT;
56
90
  let replace = false;
57
91
  let now = false;
58
92
  const participants = [];
@@ -66,14 +100,11 @@ export const catchCommand = {
66
100
  index += 1;
67
101
  }
68
102
  else if (argv[index] === '--mention') {
69
- const value = argv[index + 1];
70
- if (value !== undefined && !value.startsWith('--')) {
71
- mention = value;
72
- index += 1;
73
- }
74
- else {
75
- mention = name;
76
- }
103
+ mention = name;
104
+ }
105
+ else if (argv[index] === '--limit') {
106
+ limit = parseBoundedLimit(argv[index + 1], '--limit', CATCH_MAX_LIMIT, `${commandPrefix(requireSquarePath(context))} catch --now --limit ${CATCH_MAX_LIMIT}`);
107
+ index += 1;
77
108
  }
78
109
  else if (argv[index] === '--replace')
79
110
  replace = true;
@@ -89,6 +120,7 @@ export const catchCommand = {
89
120
  return {
90
121
  ...(participants.length > 0 ? { participants } : {}),
91
122
  ...(mention === undefined ? {} : { mention }),
123
+ limit,
92
124
  ...(idleMs === undefined ? {} : { idleMs }),
93
125
  ...(replace ? { replace } : {}),
94
126
  ...(now ? { now } : {}),
@@ -135,8 +167,7 @@ function historyContinuationArgs(argv) {
135
167
  }
136
168
  function parseHistory(argv, context) {
137
169
  const squarePath = requireSquarePath(context);
138
- const viewer = context.name;
139
- let lastN = 10;
170
+ let lastN = HISTORY_DEFAULT_LIMIT;
140
171
  let lastNExplicit = false;
141
172
  let after;
142
173
  let afterIndex;
@@ -145,7 +176,6 @@ function parseHistory(argv, context) {
145
176
  let beforeContext;
146
177
  let afterContext;
147
178
  let mention;
148
- let pending = false;
149
179
  let noTruncate = false;
150
180
  let grep;
151
181
  let fixed;
@@ -156,14 +186,7 @@ function parseHistory(argv, context) {
156
186
  for (let index = 0; index < argv.length; index++) {
157
187
  const flag = argv[index];
158
188
  if (flag === '--limit') {
159
- const value = argv[index + 1];
160
- const retry = `${commandPrefix(squarePath)} history --limit 30`;
161
- if (value === undefined || value.startsWith('--'))
162
- fail(`✕ --limit needs a positive number\n» ${retry}`);
163
- if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
164
- fail(`✕ --limit needs a positive number\n» ${retry}`);
165
- }
166
- lastN = Number(value);
189
+ lastN = parseBoundedLimit(argv[index + 1], '--limit', HISTORY_MAX_LIMIT, `${commandPrefix(squarePath)} history --limit ${HISTORY_MAX_LIMIT}`);
167
190
  lastNExplicit = true;
168
191
  index += 1;
169
192
  }
@@ -210,8 +233,6 @@ function parseHistory(argv, context) {
210
233
  mention = requireValue(argv, index, flag);
211
234
  index += 1;
212
235
  }
213
- else if (flag === '--pending')
214
- pending = true;
215
236
  else if (flag === '--grep') {
216
237
  grep = requireValue(argv, index, flag);
217
238
  index += 1;
@@ -236,15 +257,13 @@ function parseHistory(argv, context) {
236
257
  else
237
258
  fail(`✕ history does not know ${flag}\n» square history --help`);
238
259
  }
239
- if (pending && !viewer)
240
- fail('--pending requires --as <name>.');
241
260
  if (grep !== undefined && fixed !== undefined)
242
261
  fail('--grep and --fixed cannot be combined.');
243
262
  if (grep === '' || fixed === '')
244
263
  fail('--grep and --fixed require non-empty text.');
245
264
  if (beforeIndex !== undefined && afterIndex !== undefined)
246
265
  fail('--before and --after cannot be combined.');
247
- if (!lastNExplicit && (atIndexes.length > 0 || pending))
266
+ if (!lastNExplicit && atIndexes.length > 0)
248
267
  lastN = null;
249
268
  return {
250
269
  lastN,
@@ -256,8 +275,6 @@ function parseHistory(argv, context) {
256
275
  beforeContext,
257
276
  afterContext,
258
277
  mention,
259
- pending,
260
- viewer,
261
278
  noTruncate,
262
279
  grep,
263
280
  fixed,
@@ -268,11 +285,14 @@ function parseHistory(argv, context) {
268
285
  };
269
286
  }
270
287
  function historyContinuationCommand(options, squarePath, direction, index) {
271
- const prefix = options.viewer === undefined ? commandPrefix(squarePath) : participantCommandPrefix(squarePath, options.viewer);
272
- const args = [...(options.continuationArgs ?? []), direction, actId(index), '--limit', String(options.lastN ?? 10)];
273
- return `${prefix} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
288
+ const args = [...(options.continuationArgs ?? []), direction, actId(index), '--limit', String(options.lastN ?? HISTORY_DEFAULT_LIMIT)];
289
+ return `${commandPrefix(squarePath)} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
290
+ }
291
+ function boundedHistoryCommand(options, squarePath) {
292
+ const args = [...(options.continuationArgs ?? []), '--limit', String(HISTORY_MAX_LIMIT)];
293
+ return `${commandPrefix(squarePath)} history ${args.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
274
294
  }
275
- function renderFields(sayNumbers, item, fields, perception) {
295
+ function renderFields(sayNumbers, item, fields) {
276
296
  return fields.map((field) => {
277
297
  switch (field) {
278
298
  case 'id': return actId(item.index);
@@ -281,14 +301,14 @@ function renderFields(sayNumbers, item, fields, perception) {
281
301
  case 'ts':
282
302
  case 'at': return formatTimestamp(item.at);
283
303
  case 'kind': return item.kind;
284
- case 'body': return perception === 'presence' ? '' : 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
304
+ case 'body': return 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
285
305
  case 'number': return item.kind === 'say' ? String(sayNumbers[item.index]) : '';
286
306
  case 'reply': return item.kind === 'say' && item.reply !== undefined ? actId(item.reply) : '';
287
307
  default: return '';
288
308
  }
289
309
  }).join('\t');
290
310
  }
291
- function jsonLine(sayNumbers, item, perception) {
311
+ function jsonLine(sayNumbers, item) {
292
312
  const act = item;
293
313
  return JSON.stringify({
294
314
  id: actId(item.index),
@@ -297,25 +317,23 @@ function jsonLine(sayNumbers, item, perception) {
297
317
  author: act.actor ?? null,
298
318
  at: act.at,
299
319
  ts: formatTimestamp(act.at),
300
- body: perception === 'presence' ? '' : 'body' in act && typeof act.body === 'string' ? act.body : '',
320
+ body: 'body' in act && typeof act.body === 'string' ? act.body : '',
301
321
  number: act.kind === 'say' ? sayNumbers[act.index] : null,
322
+ mentions: act.kind === 'say' ? [...(act.mentions ?? [])] : [],
302
323
  reach: act.kind === 'say' ? act.reach ?? null : null,
303
324
  reply: act.kind === 'say' && act.reply !== undefined ? actId(act.reply) : null,
304
325
  });
305
326
  }
306
- function renderHistoryProjection(projection, visible, noTruncate, squarePath, viewer, mode) {
327
+ function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
307
328
  const shown = visible.filter((activity) => activity.kind === 'say' || activity.kind === 'done');
308
- const preview = noTruncate ? undefined : 200;
329
+ const preview = noTruncate || shown.length <= 1 ? undefined : 200;
309
330
  const chunks = [];
310
331
  for (const activity of shown) {
311
332
  const options = {
312
333
  preview,
313
334
  actNumber: activity.kind === 'say' ? projection.sayNumbers[activity.index] : undefined,
314
- perception: activity.perception,
315
335
  };
316
- const rendered = mode === 'archive'
317
- ? renderEventCli(activity, options)
318
- : renderAmbientEvent(activity, viewer, options);
336
+ const rendered = renderEventCli(activity, options);
319
337
  if (rendered !== '')
320
338
  chunks.push(rendered);
321
339
  const participants = projection.presenceAnchors[activity.index];
@@ -324,7 +342,7 @@ function renderHistoryProjection(projection, visible, noTruncate, squarePath, vi
324
342
  }
325
343
  if (chunks.length === 0)
326
344
  return 'latest\n ○ no public activity in this view';
327
- if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview && (mode === 'archive' || activity.perception === 'full'))) {
345
+ if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview)) {
328
346
  chunks.push(`» ${commandPrefix(squarePath)} history --no-truncate`);
329
347
  }
330
348
  return chunks.join('\n\n');
@@ -339,6 +357,9 @@ export const historyCommand = {
339
357
  // then --order only changes how the selected page is displayed.
340
358
  const projection = await historyPresentation(square, { ...options, order: 'asc' });
341
359
  let events = [...projection.activities];
360
+ if (options.lastN === null && events.length > HISTORY_MAX_LIMIT) {
361
+ fail(`✕ history is capped at ${HISTORY_MAX_LIMIT} activities\n» ${boundedHistoryCommand(options, squarePath)}`);
362
+ }
342
363
  const searching = options.grep !== undefined || options.fixed !== undefined;
343
364
  const totalMatches = searching ? events.length : 0;
344
365
  if (options.lastN != null) {
@@ -349,16 +370,14 @@ export const historyCommand = {
349
370
  if (options.order === 'desc')
350
371
  events.reverse();
351
372
  if (options.json)
352
- return events.map((item) => jsonLine(projection.sayNumbers, item, item.perception)).join('\n') + (events.length > 0 ? '\n' : '');
373
+ return events.map((item) => jsonLine(projection.sayNumbers, item)).join('\n') + (events.length > 0 ? '\n' : '');
353
374
  if (options.format !== undefined && options.format.length > 0) {
354
- return events.map((item) => renderFields(projection.sayNumbers, item, options.format, item.perception)).join('\n') + (events.length > 0 ? '\n' : '');
375
+ return events.map((item) => renderFields(projection.sayNumbers, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
355
376
  }
356
377
  const pattern = options.grep ?? options.fixed;
357
- const anonymous = options.viewer === undefined;
358
- const archive = anonymous;
359
378
  const output = pattern === undefined || pattern === ''
360
- ? renderHistoryProjection(projection, events, options.noTruncate === true, squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
361
- : renderGrepActivitiesView(events, totalMatches, options.noTruncate, squarePath, pattern, options.fixed !== undefined, (item) => projection.activities.find((candidate) => candidate.index === item.index)?.perception ?? 'full');
379
+ ? renderHistoryProjection(projection, events, options.noTruncate === true, squarePath)
380
+ : renderGrepActivitiesView(events, totalMatches, options.noTruncate, squarePath, pattern, options.fixed !== undefined, () => 'full');
362
381
  const publicEvents = events.filter((item) => item.kind === 'say' || item.kind === 'done');
363
382
  const allPublic = projection.activities.filter((item) => item.kind === 'say' || item.kind === 'done');
364
383
  const pageMin = publicEvents.length === 0 ? undefined : Math.min(...publicEvents.map((item) => item.index));
@@ -377,23 +396,43 @@ export const historyCommand = {
377
396
  },
378
397
  present: (result) => process.stdout.write(result),
379
398
  };
399
+ function participantsLimitCommand(squarePath, limit) {
400
+ return `${commandPrefix(squarePath)} participants --limit ${limit}`;
401
+ }
402
+ function parseParticipants(argv, context) {
403
+ let limit = PARTICIPANTS_DEFAULT_LIMIT;
404
+ for (let index = 0; index < argv.length; index++) {
405
+ if (argv[index] !== '--limit')
406
+ usage(context.command);
407
+ limit = parseBoundedLimit(argv[index + 1], '--limit', PARTICIPANTS_MAX_LIMIT, participantsLimitCommand(requireSquarePath(context), PARTICIPANTS_MAX_LIMIT));
408
+ index += 1;
409
+ }
410
+ return { limit };
411
+ }
380
412
  export const participantsCommand = {
381
- parse(argv, context) { if (argv.length > 0)
382
- usage(context.command); return undefined; },
383
- async execute(_intent, context) {
413
+ parse(argv, context) { return parseParticipants(argv, context); },
414
+ async execute(intent, context) {
384
415
  const squarePath = requireSquarePath(context);
385
416
  const square = await openSquare(squarePath, { clock: nowMs });
386
417
  try {
387
418
  const now = nowMs();
388
419
  const participants = await participantsPresentation(square);
389
- const lines = participants.map((participant) => {
420
+ const lines = participants.slice(0, intent.limit).map((participant) => {
390
421
  const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 ? '●' : '○';
391
422
  const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
392
423
  const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
393
- return ` ${glyph} ${participantIdentity(participant.name)} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
424
+ return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
394
425
  });
395
426
  const participantCount = participants.filter((participant) => participant.state === 'active').length;
396
- return withPathOutput(squarePath, ['participants', ...lines].join('\n'), {
427
+ const tail = participants.length <= intent.limit
428
+ ? []
429
+ : [
430
+ ` ○ ${lines.length} of ${participants.length} participants shown`,
431
+ ...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
432
+ `» ${participantsLimitCommand(squarePath, participants.length)}`,
433
+ ] : []),
434
+ ];
435
+ return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
397
436
  participantCount,
398
437
  });
399
438
  }
@@ -440,7 +479,9 @@ export const statusCommand = {
440
479
  });
441
480
  if (active.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
442
481
  people.push(` ○ … ${active.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more participants`);
443
- people.push(`» ${commandPrefix(squarePath)} participants`);
482
+ people.push(`» ${result.participants.length <= PARTICIPANTS_MAX_LIMIT
483
+ ? participantsLimitCommand(squarePath, result.participants.length)
484
+ : `${commandPrefix(squarePath)} participants`}`);
444
485
  }
445
486
  const cap = result.hardCap === null ? 'unlimited' : String(result.hardCap);
446
487
  const hold = result.holdActive
@@ -462,8 +503,7 @@ export const statusCommand = {
462
503
  : ' · latest activity is private to another participant']
463
504
  : [` ${visible.replace(/\n/g, '\n ')}`];
464
505
  if (visible.includes('more chars') && result.latestAct !== undefined) {
465
- const prefix = context.name === undefined ? commandPrefix(squarePath) : participantCommandPrefix(squarePath, context.name);
466
- latest.push(`» ${prefix} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
506
+ latest.push(`» ${commandPrefix(squarePath)} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
467
507
  }
468
508
  const output = [
469
509
  `${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
@@ -481,6 +521,9 @@ export const inboxCommand = {
481
521
  parse(argv, context) {
482
522
  let sessionId;
483
523
  let json = false;
524
+ let limitValue;
525
+ let hasLimit = false;
526
+ let duplicateLimit = false;
484
527
  for (let index = 0; index < argv.length; index++) {
485
528
  if (argv[index] === '--for-session') {
486
529
  sessionId = requireValue(argv, index, argv[index]);
@@ -488,16 +531,59 @@ export const inboxCommand = {
488
531
  }
489
532
  else if (argv[index] === '--json')
490
533
  json = true;
534
+ else if (argv[index] === '--limit') {
535
+ if (hasLimit)
536
+ duplicateLimit = true;
537
+ hasLimit = true;
538
+ const value = argv[index + 1];
539
+ if (value !== undefined && !value.startsWith('--')) {
540
+ limitValue = value;
541
+ index += 1;
542
+ }
543
+ }
491
544
  else
492
545
  usage(context.command);
493
546
  }
494
547
  if (!sessionId)
495
548
  fail('inbox requires --for-session <session-id>.');
496
- return { sessionId, json };
549
+ const retry = inboxLimitCommand(sessionId, json);
550
+ if (duplicateLimit)
551
+ fail(`✕ inbox accepts one --limit\n» ${retry}`);
552
+ const limit = hasLimit
553
+ ? parseBoundedLimit(limitValue, '--limit', INBOX_MAX_LIMIT, retry)
554
+ : INBOX_DEFAULT_LIMIT;
555
+ return { sessionId, json, limit };
497
556
  },
498
557
  async execute(intent) {
499
558
  const inbox = await sessionInbox(intent.sessionId);
500
- return intent.json ? `${JSON.stringify(inbox)}\n` : inbox.map((membership) => `${membership.name}\t${membership.squarePath}\t${membership.notifications.length}\n`).join('');
559
+ const ordered = [...inbox].sort((left, right) => {
560
+ if (left.squarePath < right.squarePath)
561
+ return -1;
562
+ if (left.squarePath > right.squarePath)
563
+ return 1;
564
+ const leftName = nameKey(left.name);
565
+ const rightName = nameKey(right.name);
566
+ if (leftName < rightName)
567
+ return -1;
568
+ if (leftName > rightName)
569
+ return 1;
570
+ if (left.name < right.name)
571
+ return -1;
572
+ if (left.name > right.name)
573
+ return 1;
574
+ return 0;
575
+ });
576
+ const rows = ordered.slice(0, intent.limit).map((membership) => ({
577
+ namePreview: inboxDisplay(membership.name),
578
+ squarePathPreview: inboxDisplay(membership.squarePath),
579
+ pending: membership.notifications.length,
580
+ }));
581
+ if (intent.json)
582
+ return `${JSON.stringify({ rows, total: ordered.length })}\n`;
583
+ return [
584
+ ...rows.map((row) => `${row.namePreview}\t${row.squarePathPreview}\t${row.pending}\n`),
585
+ ...(rows.length < ordered.length ? [`${rows.length} of ${ordered.length} memberships shown\n`] : []),
586
+ ].join('');
501
587
  },
502
588
  present: (result) => process.stdout.write(result),
503
589
  };
@@ -1,16 +1,21 @@
1
1
  import { helpRequest } from '../help.js';
2
2
  import { isSquareError } from '../model.js';
3
+ import { commandPrefix } from '../presentation.js';
3
4
  import { defaultContext, parseGlobalArgs } from './context.js';
4
5
  import { executeRegisteredCommand, findCommand } from './registry.js';
5
- function handleSquareError(error) {
6
+ function handleSquareError(error, squarePath) {
6
7
  if (isSquareError(error)) {
7
8
  process.stderr.write(`${error.message}\n`);
9
+ if (squarePath !== undefined && /^(Unknown participant|Unknown mention target)/.test(error.message)) {
10
+ process.stderr.write(`» ${commandPrefix(squarePath)} participants\n`);
11
+ }
8
12
  process.exit(error.code === 'not_found' ? 1 : 2);
9
13
  }
10
14
  throw error;
11
15
  }
12
16
  /** Parse global flags, select an executable adapter, and leave all command work to the registry. */
13
17
  export async function runCli(rawArgs = process.argv.slice(2)) {
18
+ let squarePath;
14
19
  try {
15
20
  const requestedHelp = helpRequest(rawArgs);
16
21
  if (requestedHelp !== undefined) {
@@ -18,6 +23,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
18
23
  return;
19
24
  }
20
25
  const parsed = await parseGlobalArgs(rawArgs);
26
+ squarePath = parsed.squarePath;
21
27
  if (parsed.args.length === 0 || parsed.args[0] === '--help' || parsed.args[0] === '-h') {
22
28
  await executeRegisteredCommand('help', [], defaultContext('help', parsed.squarePath, parsed.name));
23
29
  return;
@@ -30,6 +36,6 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
30
36
  await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
31
37
  }
32
38
  catch (error) {
33
- handleSquareError(error);
39
+ handleSquareError(error, squarePath);
34
40
  }
35
41
  }
@@ -8,7 +8,7 @@ interface BuildIntent {
8
8
  }
9
9
  interface JoinIntent {
10
10
  name: string;
11
- lastN: number | null;
11
+ lastN: number;
12
12
  kick: boolean;
13
13
  }
14
14
  interface ActivityIntent {
@@ -16,6 +16,8 @@ interface ActivityIntent {
16
16
  activity: string;
17
17
  force: boolean;
18
18
  noWait: boolean;
19
+ noMention: boolean;
20
+ mentions: string[];
19
21
  reach?: Reach;
20
22
  reply?: number;
21
23
  }
@@ -1,7 +1,7 @@
1
1
  import { cmdActivity } from '../activity.js';
2
2
  import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
3
- import { participantCommandPrefix, participantIdentity, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
4
- import { hasAutomaticDeliveryIdentity, } from '../registry.js';
3
+ import { participantCommandPrefix, participantIdentity, quoteShell, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
4
+ import { hasAutomaticDeliveryIdentity, lookupParticipant, recordSessionDone, } from '../registry.js';
5
5
  import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
6
6
  import { projectLocalParticipantBinding, sessionIdsFromEnvironment } from '../square-projections.js';
7
7
  import { nowMs } from '../runtime.js';
@@ -10,7 +10,9 @@ import { closeOpenSquare } from '../open-square.js';
10
10
  import { openParticipant, Square } from '../square-wiring.js';
11
11
  import { entryPresentation, eventPresentation } from '../views.js';
12
12
  import { createDefaultWakeTransport } from '../notifications.js';
13
- import { fail, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
13
+ import { fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
14
+ const JOIN_DEFAULT_LAST = 10;
15
+ const JOIN_MAX_LAST = 100;
14
16
  function parseBuild(argv) {
15
17
  const options = { force: false, hardCap: null };
16
18
  for (let index = 0; index < argv.length; index++) {
@@ -60,16 +62,15 @@ export const buildCommand = {
60
62
  present: (result) => process.stdout.write(result),
61
63
  };
62
64
  function parseJoin(argv, context) {
63
- let lastN = 10;
65
+ const name = requireParticipant(context.name);
66
+ const squarePath = requireSquarePath(context);
67
+ let lastN = JOIN_DEFAULT_LAST;
64
68
  let kick = false;
65
69
  for (let index = 0; index < argv.length; index++) {
66
70
  if (argv[index] === '--last') {
67
- lastN = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
71
+ lastN = parseBoundedLimit(argv[index + 1], '--last', JOIN_MAX_LAST, `${participantCommandPrefix(squarePath, name)} join --last ${JOIN_MAX_LAST}`);
68
72
  index += 1;
69
73
  }
70
- else if (argv[index] === '--all') {
71
- lastN = null;
72
- }
73
74
  else if (argv[index] === '--kick') {
74
75
  kick = true;
75
76
  }
@@ -77,7 +78,7 @@ function parseJoin(argv, context) {
77
78
  usage(context.command);
78
79
  }
79
80
  }
80
- return { name: requireParticipant(context.name), lastN, kick };
81
+ return { name, lastN, kick };
81
82
  }
82
83
  export const joinCommand = {
83
84
  parse: parseJoin,
@@ -96,17 +97,27 @@ export const joinCommand = {
96
97
  participant: intent.name,
97
98
  sessionIds: sessionIdsFromEnvironment(),
98
99
  }) !== undefined;
99
- const participant = await square.join(intent.name);
100
- const joinedName = participant.name;
101
100
  const isRejoin = before.joined;
102
101
  if (isRejoin && !intent.kick && !reconnect) {
103
102
  fail([
104
- `✕ ${participantIdentity(joinedName)} shoos you out of the square`,
103
+ `✕ ${participantIdentity(intent.name)} shoos you out of the square`,
105
104
  ` · a same-named participant stands here — the name is taken`,
106
105
  ` · --kick banishes her and the name becomes yours`,
107
- `» ${participantCommandPrefix(squarePath, joinedName)} join --kick`,
106
+ `» ${participantCommandPrefix(squarePath, intent.name)} join --kick`,
108
107
  ].join('\n'));
109
108
  }
109
+ const takeoverNeeded = isRejoin && intent.kick && !reconnect;
110
+ const oldBindings = takeoverNeeded ? await lookupParticipant(squarePath, intent.name) : [];
111
+ const participant = takeoverNeeded ? await square.takeover(intent.name, oldBindings.map((binding) => binding.sessionId)) : await square.join(intent.name);
112
+ const joinedName = participant.name;
113
+ if (takeoverNeeded) {
114
+ const currentSessions = new Set(sessionIdsFromEnvironment());
115
+ for (const binding of oldBindings) {
116
+ if (!currentSessions.has(binding.sessionId)) {
117
+ await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel).catch(() => undefined);
118
+ }
119
+ }
120
+ }
110
121
  const afterSquare = await openSquare(squarePath, { clock: nowMs });
111
122
  const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
112
123
  await closeOpenSquare(afterSquare);
@@ -146,6 +157,8 @@ function parseActivity(argv, context) {
146
157
  let force = false;
147
158
  let noWait = false;
148
159
  let bell = false;
160
+ let noMention = false;
161
+ const mentions = [];
149
162
  let reply;
150
163
  const bodyArgs = [];
151
164
  for (let index = 0; index < argv.length; index++) {
@@ -158,6 +171,12 @@ function parseActivity(argv, context) {
158
171
  fail('✕ express does not know --beside\n» square express --help');
159
172
  else if (argument === '--bell')
160
173
  bell = true;
174
+ else if (argument === '--no-mention')
175
+ noMention = true;
176
+ else if (argument === '--mention') {
177
+ mentions.push(requireParticipant(requireValue(argv, index, argument)));
178
+ index += 1;
179
+ }
161
180
  else if (argument === '--reply') {
162
181
  const replyIndex = parseActivityId(requireValue(argv, index, argument));
163
182
  if (replyIndex === undefined)
@@ -169,24 +188,31 @@ function parseActivity(argv, context) {
169
188
  bodyArgs.push(argument);
170
189
  }
171
190
  const reach = bell ? 'bell' : undefined;
191
+ if (bell && (noMention || mentions.length > 0))
192
+ fail('✕ --bell cannot be combined with --mention or --no-mention\n» square express --help');
193
+ if (noMention && mentions.length > 0)
194
+ fail('✕ --no-mention cannot be combined with --mention\n» square express --help');
195
+ if (!bell && !noMention && mentions.length === 0)
196
+ fail('✕ express needs --mention <name>, --no-mention, or --bell\n» square express --help');
172
197
  if (bodyArgs.length !== 1) {
173
198
  if (bodyArgs.length === 0) {
174
199
  if (!process.stdin.isTTY)
175
- return { name: requireParticipant(context.name), activity: '-', force, noWait, reach, reply };
200
+ return { name: requireParticipant(context.name), activity: '-', force, noWait, noMention, mentions, reach, reply };
176
201
  }
177
202
  fail("express requires a body argument (a quoted string or '-' with piped stdin)");
178
203
  }
179
- return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, reach, reply };
204
+ return { name: requireParticipant(context.name), activity: bodyArgs[0], force, noWait, noMention, mentions, reach, reply };
180
205
  }
181
206
  export const expressCommand = {
182
207
  parse: parseActivity,
183
208
  async execute(intent, context) {
184
209
  const squarePath = requireSquarePath(context);
185
210
  const body = await resolveBody(intent.activity);
186
- const reachArg = intent.reach === 'bell' ? ' --bell' : '';
211
+ const reachArg = intent.reach === 'bell' ? ' --bell' : intent.noMention ? ' --no-mention' : intent.mentions.map((name) => ` --mention ${quoteShell(name)}`).join('');
187
212
  await cmdActivity(squarePath, intent.name, body, (value) => value, {
188
213
  force: intent.force,
189
214
  noWait: intent.noWait,
215
+ mentions: intent.mentions,
190
216
  reach: intent.reach,
191
217
  reply: intent.reply,
192
218
  forceCommand: `${participantCommandPrefix(squarePath, intent.name)} express --force${reachArg}${intent.reply === undefined ? '' : ` --reply ${formatActivityId(intent.reply)}`} -`,
@@ -10,6 +10,7 @@ export declare class CodexQueueSendError extends Error {
10
10
  constructor(message: string, kind: CodexQueueFailureKind);
11
11
  }
12
12
  export declare function sendCodexQueue({ threadId, message }: CodexQueueRequest, opts?: {
13
+ args?: string[];
13
14
  bin?: string;
14
15
  env?: NodeJS.ProcessEnv;
15
16
  timeoutMs?: number;
@@ -16,7 +16,7 @@ function classifyFailure(message) {
16
16
  return 'unknown';
17
17
  }
18
18
  export function sendCodexQueue({ threadId, message }, opts = {}) {
19
- const result = spawnSync(opts.bin ?? process.env.SQUARE_CODEX_BIN ?? 'codex', ['queue', '--thread', threadId, '--message', message], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env: opts.env ?? process.env });
19
+ const result = spawnSync(opts.bin ?? process.env.SQUARE_CODEX_BIN ?? 'codex', [...(opts.args ?? []), 'queue', '--thread', threadId, '--message', message], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env: opts.env ?? process.env });
20
20
  if (result.error) {
21
21
  const messageText = result.error.message;
22
22
  throw new CodexQueueSendError(messageText, classifyFailure(messageText));
@@ -63,6 +63,7 @@ export declare function decideAct(squareState: SquareState, input: {
63
63
  body: string;
64
64
  force: boolean;
65
65
  now: number;
66
+ mentions?: readonly string[];
66
67
  reach?: Reach;
67
68
  reply?: number;
68
69
  }): ActDecision;