@astrosheep/square 0.3.70 → 0.3.72

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 (126) hide show
  1. package/dist/activity-feed.d.ts +4 -5
  2. package/dist/activity-feed.js +14 -10
  3. package/dist/artifact.d.ts +26 -33
  4. package/dist/artifact.js +346 -317
  5. package/dist/attention-presentation.js +2 -9
  6. package/dist/automatic-session.js +6 -6
  7. package/dist/boundary-presentation.js +3 -3
  8. package/dist/catch-decisions.d.ts +19 -12
  9. package/dist/catch-decisions.js +32 -68
  10. package/dist/claude-delivery.js +7 -7
  11. package/dist/claude-mod.js +18 -7
  12. package/dist/cli/catch-presentation.d.ts +7 -0
  13. package/dist/cli/catch-presentation.js +55 -0
  14. package/dist/cli/context.d.ts +1 -1
  15. package/dist/cli/context.js +5 -23
  16. package/dist/cli/express-presentation.d.ts +10 -0
  17. package/dist/cli/express-presentation.js +196 -0
  18. package/dist/cli/maintenance-commands.d.ts +4 -0
  19. package/dist/cli/maintenance-commands.js +29 -6
  20. package/dist/cli/observation-commands.d.ts +7 -5
  21. package/dist/cli/observation-commands.js +275 -262
  22. package/dist/cli/program.d.ts +1 -1
  23. package/dist/cli/program.js +43 -56
  24. package/dist/cli/registry.js +2 -1
  25. package/dist/cli/square-commands.d.ts +3 -9
  26. package/dist/cli/square-commands.js +134 -255
  27. package/dist/codex-queue.js +4 -1
  28. package/dist/decisions.d.ts +26 -74
  29. package/dist/decisions.js +190 -335
  30. package/dist/delivery-health.js +13 -22
  31. package/dist/delivery-operations.d.ts +0 -11
  32. package/dist/delivery-operations.js +44 -33
  33. package/dist/delivery.d.ts +33 -32
  34. package/dist/delivery.js +70 -81
  35. package/dist/help.js +6 -1
  36. package/dist/inbox.d.ts +27 -0
  37. package/dist/inbox.js +55 -18
  38. package/dist/index.d.ts +2 -4
  39. package/dist/index.js +1 -1
  40. package/dist/list.d.ts +7 -1
  41. package/dist/list.js +49 -122
  42. package/dist/mcp.d.ts +1 -1
  43. package/dist/mcp.js +7 -37
  44. package/dist/model.d.ts +4 -44
  45. package/dist/model.js +1 -3
  46. package/dist/notifications.d.ts +1 -1
  47. package/dist/notifications.js +13 -7
  48. package/dist/open-square.d.ts +16 -2
  49. package/dist/open-square.js +40 -1
  50. package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
  51. package/dist/packages/agent-delivery/src/index.d.ts +2 -42
  52. package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
  53. package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
  54. package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
  55. package/dist/packages/agent-delivery/src/types.d.ts +42 -0
  56. package/dist/participant-host.d.ts +6 -3
  57. package/dist/participant-host.js +44 -38
  58. package/dist/perception-projection.d.ts +6 -15
  59. package/dist/perception-projection.js +6 -26
  60. package/dist/plugins/opencode/delivery.js +9 -9
  61. package/dist/plugins/opencode/index.js +3 -3
  62. package/dist/ports.d.ts +156 -11
  63. package/dist/presentation-operations.d.ts +1 -1
  64. package/dist/presentation-operations.js +35 -44
  65. package/dist/presentation.d.ts +12 -19
  66. package/dist/presentation.js +33 -54
  67. package/dist/registry.d.ts +2 -0
  68. package/dist/registry.js +1 -1
  69. package/dist/routes.d.ts +7 -14
  70. package/dist/routes.js +34 -72
  71. package/dist/runtime.d.ts +12 -39
  72. package/dist/runtime.js +33 -164
  73. package/dist/square-actions.d.ts +11 -21
  74. package/dist/square-actions.js +171 -275
  75. package/dist/square-application.d.ts +31 -55
  76. package/dist/square-application.js +625 -141
  77. package/dist/square-core.d.ts +3 -94
  78. package/dist/square-core.js +8 -353
  79. package/dist/square-domain.d.ts +24 -0
  80. package/dist/square-domain.js +77 -0
  81. package/dist/square-facade.d.ts +360 -9
  82. package/dist/square-migration.d.ts +24 -0
  83. package/dist/square-migration.js +211 -0
  84. package/dist/square-projections.d.ts +2 -42
  85. package/dist/square-projections.js +1 -37
  86. package/dist/square-sqlite.d.ts +59 -0
  87. package/dist/square-sqlite.js +702 -0
  88. package/dist/stream.d.ts +3 -10
  89. package/dist/stream.js +81 -52
  90. package/dist/views.d.ts +27 -95
  91. package/dist/views.js +217 -87
  92. package/dist/wake-eligibility.d.ts +15 -5
  93. package/dist/wake-eligibility.js +32 -16
  94. package/dist/wakes.d.ts +3 -3
  95. package/dist/wakes.js +31 -27
  96. package/guides/application-facade-delivery-evidence.md +200 -0
  97. package/guides/application-facade-design.md +95 -0
  98. package/guides/change-waits.md +4 -4
  99. package/guides/migration.md +77 -0
  100. package/guides/postcommit-simplification-performance.md +36 -0
  101. package/guides/primitive-catch-consolidation.md +85 -0
  102. package/guides/primitive-history-consolidation.md +76 -0
  103. package/guides/primitive-status-consolidation.md +87 -0
  104. package/guides/primitive-storage-consolidation.md +128 -0
  105. package/guides/row-runtime-delivery-evidence.md +160 -0
  106. package/guides/row-storage-design.md +567 -0
  107. package/package.json +1 -1
  108. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  109. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  110. package/plugins/opencode/delivery.ts +11 -11
  111. package/plugins/opencode/index.ts +3 -3
  112. package/plugins/paseo/client/square-message.tsx +77 -64
  113. package/plugins/paseo/shared/message.ts +16 -0
  114. package/plugins/pi/square-pi.js +5 -1
  115. package/dist/activity.d.ts +0 -14
  116. package/dist/activity.js +0 -248
  117. package/dist/square-file-adapter.d.ts +0 -26
  118. package/dist/square-file-adapter.js +0 -85
  119. package/dist/square-state.d.ts +0 -6
  120. package/dist/square-state.js +0 -138
  121. package/dist/square-storage.d.ts +0 -14
  122. package/dist/square-storage.js +0 -188
  123. package/dist/square-wiring.d.ts +0 -33
  124. package/dist/square-wiring.js +0 -109
  125. package/dist/watch.d.ts +0 -8
  126. package/dist/watch.js +0 -252
@@ -1,27 +1,15 @@
1
+ import path from 'node:path';
2
+ import { once } from 'node:events';
1
3
  import { runClaudeMod } from '../claude-mod.js';
2
4
  import { runCodexHookAsync } from '../codex-hook.js';
3
- import { sessionInbox } from '../inbox.js';
4
- import { sweepPendingNotifications } from '../notifications.js';
5
- import { cmdListSquares } from '../list.js';
6
- import { nameKey, parseActivityId, sameName } from '../model.js';
5
+ import { isSquareError, sameName } from '../model.js';
7
6
  import { commandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEventFacts, renderPresenceAnchor, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
8
- import { actId, nowMs } from '../runtime.js';
9
7
  import { style } from '../tty-style.js';
10
- import { cmdStream, cmdStreamNdjson } from '../stream.js';
11
8
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
12
- import { cmdWatch } from '../watch.js';
13
- import { openSquare } from '../square-file-adapter.js';
14
- import { closeOpenSquare } from '../open-square.js';
15
- import { createSquareApplication } from '../square-application.js';
16
- import { historyPresentation } from '../views.js';
17
- import { hostLedgerForEnv } from '../registry.js';
18
- import { fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
19
- import { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from '../catch-decisions.js';
9
+ import { formatActivityId, parseActivityId, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from '../square-application.js';
10
+ import { renderCatchOutcome, renderCatchProgress } from './catch-presentation.js';
11
+ import { applicationFor as application, fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireSquarePath, requireValue, usage, } from './context.js';
20
12
  const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
21
- /** A participant counts as here when their last sign of life sits inside this window. */
22
- const STATUS_HERE_WINDOW_MS = 30 * 60_000;
23
- const HISTORY_DEFAULT_LIMIT = 10;
24
- const HISTORY_MAX_LIMIT = 100;
25
13
  const PARTICIPANTS_DEFAULT_LIMIT = 20;
26
14
  const PARTICIPANTS_MAX_LIMIT = 100;
27
15
  const INBOX_DEFAULT_LIMIT = 20;
@@ -31,12 +19,71 @@ function inboxDisplay(value) {
31
19
  const truncated = truncateChars(value, INBOX_DISPLAY_CHARS - 1);
32
20
  return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
33
21
  }
22
+ function renderSquareList(data, projection) {
23
+ const { items, stopped, remaining, next } = data;
24
+ if (data.total === 0) {
25
+ return ['(no squares found)', ...(stopped ? [style('dim', '○ the walk stopped early — deeper squares may still be hiding')] : [])].join('\n') + '\n';
26
+ }
27
+ const lines = ['squares'];
28
+ for (const item of items) {
29
+ const glyph = item.activities > 0 ? '●' : style('dim', '○');
30
+ lines.push(`${glyph} ${inboxDisplay(item.path)}${style('dim', ` · ${formatRelativeTime(item.lastActiveAt, projection.now)} · ${item.participants.length} in the square · ${item.activities} ${item.activities === 1 ? 'activity' : 'activities'}`)}`);
31
+ const context = item.context.slice(0, 2);
32
+ if (context.length === 0)
33
+ lines.push(` ${style('dim', 'context')} · (none)`);
34
+ else {
35
+ context.forEach((line, index) => lines.push(` ${index === 0 ? style('dim', 'context') : ' '} · ${inboxDisplay(line)}`));
36
+ const hidden = item.context.length - context.length;
37
+ if (hidden > 0)
38
+ lines.push(style('dim', ` · … ${hidden} more ${hidden === 1 ? 'line' : 'lines'}`));
39
+ }
40
+ const participants = item.participants.slice(0, 3);
41
+ const hidden = item.participants.length - participants.length;
42
+ const names = participants.length === 0 ? 'nobody' : participants.map((name) => participantIdentity(inboxDisplay(name))).join(' · ');
43
+ lines.push(` ${style('dim', 'participants')} · ${names}${hidden > 0 ? style('dim', ` · … ${hidden} more`) : ''}`);
44
+ }
45
+ if (remaining > 0) {
46
+ lines.push(style('dim', stopped ? ' … more squares' : ` … ${remaining} more ${remaining === 1 ? 'square' : 'squares'}`));
47
+ if (next !== undefined)
48
+ lines.push(`square list --depth ${projection.depth} --limit ${projection.limit} --after ${quoteShell(next)}`);
49
+ }
50
+ if (stopped)
51
+ lines.push(style('dim', '○ the walk stopped early — deeper squares may still be hiding'));
52
+ return lines.join('\n') + '\n';
53
+ }
34
54
  export const listCommand = {
35
- parse: (argv) => argv,
36
- async execute(argv, context) {
37
- await cmdListSquares(argv, () => usage(context.command));
55
+ parse(argv, context) {
56
+ let depth = DEFAULT_LIST_DEPTH;
57
+ let limit = DEFAULT_LIST_LIMIT;
58
+ let after;
59
+ for (let index = 0; index < argv.length; index += 2) {
60
+ const flag = argv[index], value = argv[index + 1];
61
+ if (value === undefined || !['--depth', '--limit', '--after'].includes(flag))
62
+ usage(context.command);
63
+ if (flag === '--after') {
64
+ if (after !== undefined || value.length === 0 || path.isAbsolute(value))
65
+ usage(context.command);
66
+ after = value;
67
+ }
68
+ else {
69
+ if (!/^\d+$/.test(value))
70
+ usage(context.command);
71
+ const number = Number(value);
72
+ if (!Number.isSafeInteger(number) || (flag === '--depth' ? number > MAX_LIST_DEPTH : number === 0 || number > MAX_LIST_LIMIT))
73
+ usage(context.command);
74
+ if (flag === '--depth')
75
+ depth = number;
76
+ else
77
+ limit = number;
78
+ }
79
+ }
80
+ return { depth, limit, after };
38
81
  },
39
- present: () => { },
82
+ async execute(intent, context) {
83
+ const outcome = await application(context).discover(intent);
84
+ return renderSquareList(outcome.data, outcome.projection);
85
+ },
86
+ present: (result) => process.stdout.write(result),
40
87
  };
41
88
  export const streamCommand = {
42
89
  parse(argv, context) {
@@ -76,17 +123,39 @@ export const streamCommand = {
76
123
  },
77
124
  async execute(intent, context) {
78
125
  const squarePath = requireSquarePath(context);
79
- if (intent.ndjson)
80
- await cmdStreamNdjson(squarePath, intent.forName, intent.start);
81
- else
82
- await cmdStream(squarePath);
126
+ if (!intent.ndjson) {
127
+ process.stderr.write(`✕ interactive stream was removed\nsquare --location ${quoteShell(path.resolve(context.cwd, squarePath))} stream --ndjson\n`);
128
+ process.exitCode = 2;
129
+ return;
130
+ }
131
+ const controller = new AbortController();
132
+ const interrupt = () => controller.abort();
133
+ process.once('SIGINT', interrupt);
134
+ try {
135
+ for await (const record of application(context).stream({ recipient: intent.forName, start: intent.start }, { signal: controller.signal })) {
136
+ if (!process.stdout.write(`${JSON.stringify(record)}\n`))
137
+ await once(process.stdout, 'drain', { signal: controller.signal });
138
+ }
139
+ }
140
+ catch (error) {
141
+ if (controller.signal.aborted)
142
+ process.exitCode = 130;
143
+ else if (isSquareError(error) && error.code === 'unavailable') {
144
+ process.stderr.write(`square not found: ${squarePath}\n`);
145
+ process.exitCode = 2;
146
+ }
147
+ else
148
+ throw error;
149
+ }
150
+ finally {
151
+ process.off('SIGINT', interrupt);
152
+ }
83
153
  },
84
154
  present: () => { },
85
155
  };
86
156
  export const catchCommand = {
87
157
  parse(argv, context) {
88
- const name = requireParticipant(context.name);
89
- let idleMs;
158
+ let idle;
90
159
  let mention;
91
160
  let limit;
92
161
  let id;
@@ -108,11 +177,11 @@ export const catchCommand = {
108
177
  index += 1;
109
178
  }
110
179
  else if (argv[index] === '--idle') {
111
- idleMs = parseDurationMs(requireValue(argv, index, argv[index]), argv[index]);
180
+ idle = parseDurationMs(requireValue(argv, index, argv[index]), argv[index]);
112
181
  index += 1;
113
182
  }
114
183
  else if (argv[index] === '--mention') {
115
- mention = name;
184
+ mention = true;
116
185
  }
117
186
  else if (argv[index] === '--limit') {
118
187
  limit = parseBoundedLimit(argv[index + 1], '--limit', CATCH_MAX_LIMIT, `${commandPrefix(requireSquarePath(context))} catch --limit ${CATCH_MAX_LIMIT}`);
@@ -129,29 +198,47 @@ export const catchCommand = {
129
198
  if (argv.some((flag) => ['--idle', '--from', '--mention', '--limit', '--replace'].includes(flag))) {
130
199
  fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\nsquare catch --help');
131
200
  }
132
- return { id, now: true };
201
+ return { id };
133
202
  }
134
- if (idleMs !== undefined && now)
203
+ if (idle !== undefined && now)
135
204
  fail('catch accepts --now or --idle, not both.');
136
- // A bare catch takes pending activity now; --now stays an explicit spelling of the same thing.
137
- if (idleMs === undefined)
138
- now = true;
139
- if (replace && now)
205
+ if (replace && idle === undefined)
140
206
  fail('--replace can only be used with --idle.');
141
207
  return {
142
- ...(participants.length > 0 ? { participants } : {}),
208
+ ...(participants.length > 0 ? { from: participants } : {}),
143
209
  ...(mention === undefined ? {} : { mention }),
144
210
  limit: limit ?? CATCH_DEFAULT_LIMIT,
145
- ...(idleMs === undefined ? {} : { idleMs }),
211
+ ...(idle === undefined ? {} : { idle }),
146
212
  ...(replace ? { replace } : {}),
147
- ...(now ? { now } : {}),
148
213
  };
149
214
  },
150
215
  async execute(intent, context) {
151
216
  const squarePath = requireSquarePath(context);
152
- const caught = await cmdWatch(squarePath, requireParticipant(context.name), intent, { cwd: context.cwd, env: context.env });
153
- if (caught !== false)
154
- await sweepPendingNotifications(squarePath);
217
+ const controller = new AbortController();
218
+ const interrupt = () => controller.abort();
219
+ process.once('SIGINT', interrupt);
220
+ try {
221
+ const outcome = await application(context).catch(intent, {
222
+ signal: controller.signal,
223
+ onProgress(progress) {
224
+ const line = renderCatchProgress(squarePath, progress);
225
+ if (line !== undefined)
226
+ process.stderr.write(line);
227
+ },
228
+ });
229
+ const rendered = renderCatchOutcome(squarePath, outcome, intent);
230
+ process.stdout.write(rendered.stdout);
231
+ process.exitCode = rendered.exitCode;
232
+ }
233
+ catch (error) {
234
+ if (!controller.signal.aborted)
235
+ throw error;
236
+ process.stdout.write(withPathOutput(squarePath, '○ you step back — catch stopped'));
237
+ process.exitCode = 130;
238
+ }
239
+ finally {
240
+ process.off('SIGINT', interrupt);
241
+ }
155
242
  },
156
243
  present: () => { },
157
244
  };
@@ -161,8 +248,8 @@ function parseActRef(value, flag) {
161
248
  fail(`Invalid ${flag}: expected an activity id like act/12`);
162
249
  return index;
163
250
  }
164
- function parseTimestamp(value, flag) {
165
- const timestamp = parseTimeOrRelative(value, nowMs());
251
+ function parseTimestamp(value, flag, now) {
252
+ const timestamp = parseTimeOrRelative(value, now);
166
253
  if (!Number.isFinite(timestamp))
167
254
  fail(`Invalid ${flag} timestamp: ${value}`);
168
255
  return timestamp;
@@ -188,12 +275,11 @@ function historyContinuationArgs(argv) {
188
275
  }
189
276
  function parseHistory(argv, context) {
190
277
  const squarePath = requireSquarePath(context);
191
- let lastN = HISTORY_DEFAULT_LIMIT;
192
- let lastNExplicit = false;
278
+ let limit;
279
+ let since;
193
280
  let after;
194
- let afterIndex;
195
- let beforeIndex;
196
- const atIndexes = [];
281
+ let before;
282
+ const at = [];
197
283
  let beforeContext;
198
284
  let afterContext;
199
285
  let mention;
@@ -207,8 +293,7 @@ function parseHistory(argv, context) {
207
293
  for (let index = 0; index < argv.length; index++) {
208
294
  const flag = argv[index];
209
295
  if (flag === '--limit') {
210
- lastN = parseBoundedLimit(argv[index + 1], '--limit', HISTORY_MAX_LIMIT, `${commandPrefix(squarePath)} history --limit ${HISTORY_MAX_LIMIT}`);
211
- lastNExplicit = true;
296
+ limit = parseBoundedLimit(argv[index + 1], '--limit', HISTORY_MAX_LIMIT, `${commandPrefix(squarePath)} history --limit ${HISTORY_MAX_LIMIT}`);
212
297
  index += 1;
213
298
  }
214
299
  else if (flag === '--from') {
@@ -216,22 +301,22 @@ function parseHistory(argv, context) {
216
301
  index += 1;
217
302
  }
218
303
  else if (flag === '--since') {
219
- after = parseTimestamp(requireValue(argv, index, flag), flag);
304
+ since = parseTimestamp(requireValue(argv, index, flag), flag, application(context).now());
220
305
  index += 1;
221
306
  }
222
307
  else if (flag === '--after') {
223
- afterIndex = parseActRef(requireValue(argv, index, flag), flag);
308
+ after = formatActivityId(parseActRef(requireValue(argv, index, flag), flag));
224
309
  index += 1;
225
310
  }
226
311
  else if (flag === '--before') {
227
- beforeIndex = parseActRef(requireValue(argv, index, flag), flag);
312
+ before = formatActivityId(parseActRef(requireValue(argv, index, flag), flag));
228
313
  index += 1;
229
314
  }
230
315
  else if (flag === '--at') {
231
316
  const values = requireValue(argv, index, flag).split(',');
232
317
  if (values.some((value) => value === ''))
233
318
  fail(`Invalid ${flag}: expected an activity id like act/12`);
234
- atIndexes.push(...values.map((value) => parseActRef(value, flag)));
319
+ at.push(...values.map((value) => formatActivityId(parseActRef(value, flag))));
235
320
  index += 1;
236
321
  }
237
322
  else if (flag === '-B') {
@@ -282,26 +367,12 @@ function parseHistory(argv, context) {
282
367
  fail('--grep and --fixed cannot be combined.');
283
368
  if (grep === '' || fixed === '')
284
369
  fail('--grep and --fixed require non-empty text.');
285
- if (beforeIndex !== undefined && afterIndex !== undefined)
370
+ if (before !== undefined && after !== undefined)
286
371
  fail('--before and --after cannot be combined.');
287
- if (!lastNExplicit && atIndexes.length > 0)
288
- lastN = null;
289
372
  return {
290
- lastN,
291
- participants,
292
- after,
293
- afterIndex,
294
- beforeIndex,
295
- atIndexes: atIndexes.length === 0 ? undefined : atIndexes,
296
- beforeContext,
297
- afterContext,
298
- mention,
299
- noTruncate,
300
- grep,
301
- fixed,
302
- order,
303
- format,
304
- json,
373
+ query: { limit, from: participants, since, after, before, at: at.length === 0 ? undefined : at,
374
+ contextBefore: beforeContext, contextAfter: afterContext, mention, grep, fixed, order },
375
+ noTruncate, format, json,
305
376
  continuationArgs: historyContinuationArgs(argv),
306
377
  };
307
378
  }
@@ -309,14 +380,10 @@ function historyContinuationCommand(options, squarePath, args) {
309
380
  const full = [...(options.continuationArgs ?? []), ...args];
310
381
  return `${commandPrefix(squarePath)} history ${full.map((arg) => arg.startsWith('-') || /^act\/\d+$/.test(arg) || /^\d+$/.test(arg) ? arg : quoteShell(arg)).join(' ')}`;
311
382
  }
312
- /** The archive-visible activity model: content plus the lifecycle moves that keep its story honest — a raised hand pairs with its lowering, and a departure pairs with the return. */
313
- function isArchiveActivity(item) {
314
- return item.kind === 'say' || item.kind === 'done' || item.kind === 'hold' || item.kind === 'resume' || item.kind === 'join';
315
- }
316
383
  function renderFields(sayNumbers, item, fields) {
317
384
  return fields.map((field) => {
318
385
  switch (field) {
319
- case 'id': return actId(item.index);
386
+ case 'id': return formatActivityId(item.index);
320
387
  case 'author':
321
388
  case 'actor': return item.actor ?? '';
322
389
  case 'ts':
@@ -324,7 +391,7 @@ function renderFields(sayNumbers, item, fields) {
324
391
  case 'kind': return item.kind;
325
392
  case 'body': return 'body' in item && typeof item.body === 'string' ? item.body.replace(/\s+/g, ' ').trim() : '';
326
393
  case 'number': return item.kind === 'say' ? String(sayNumbers[item.index]) : '';
327
- case 'reply': return item.kind === 'say' && item.reply !== undefined ? actId(item.reply) : '';
394
+ case 'reply': return item.kind === 'say' && item.reply !== undefined ? formatActivityId(item.reply) : '';
328
395
  default: return '';
329
396
  }
330
397
  }).join('\t');
@@ -332,7 +399,7 @@ function renderFields(sayNumbers, item, fields) {
332
399
  function jsonLine(sayNumbers, item) {
333
400
  const act = item;
334
401
  return JSON.stringify({
335
- id: actId(item.index),
402
+ id: formatActivityId(item.index),
336
403
  index: item.index,
337
404
  kind: act.kind,
338
405
  author: act.actor ?? null,
@@ -342,11 +409,11 @@ function jsonLine(sayNumbers, item) {
342
409
  number: act.kind === 'say' ? sayNumbers[act.index] : null,
343
410
  mentions: act.kind === 'say' ? [...(act.mentions ?? [])] : [],
344
411
  reach: act.kind === 'say' ? act.reach ?? null : null,
345
- reply: act.kind === 'say' && act.reply !== undefined ? actId(act.reply) : null,
412
+ reply: act.kind === 'say' && act.reply !== undefined ? formatActivityId(act.reply) : null,
346
413
  });
347
414
  }
348
415
  function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
349
- const shown = visible.filter(isArchiveActivity);
416
+ const shown = visible;
350
417
  const preview = noTruncate || shown.length <= 1 ? undefined : 200;
351
418
  const chunks = [];
352
419
  // Presence anchors collect into one closing line instead of interrupting the page.
@@ -377,51 +444,30 @@ export const historyCommand = {
377
444
  parse(argv, context) { return parseHistory(argv, context); },
378
445
  async execute(options, context) {
379
446
  const squarePath = requireSquarePath(context);
380
- const square = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
447
+ let outcome;
381
448
  try {
382
- // Keep the projection chronological; pagination chooses a stable edge,
383
- // then --order only changes how the selected page is displayed. The
384
- // plain archive hides `read` bookkeeping, while --at/-C/--since windows
385
- // render the raw activity stream around their coordinates.
386
- const projection = await historyPresentation(square, { ...options, order: 'asc' });
387
- const hideReadActs = options.atIndexes === undefined && options.beforeContext === undefined && options.afterContext === undefined && options.after === undefined;
388
- let events = hideReadActs ? projection.activities.filter((activity) => activity.kind !== 'read') : [...projection.activities];
389
- if (options.lastN === null && events.length > HISTORY_MAX_LIMIT) {
390
- fail(`✕ history is capped at ${HISTORY_MAX_LIMIT} activities\n${historyContinuationCommand(options, squarePath, ['--limit', String(HISTORY_MAX_LIMIT)])}`);
391
- }
392
- const searching = options.grep !== undefined || options.fixed !== undefined;
393
- const totalMatches = searching ? events.length : 0;
394
- if (options.lastN != null) {
395
- events = options.afterIndex !== undefined
396
- ? events.slice(0, options.lastN)
397
- : events.slice(-options.lastN);
398
- }
399
- if (options.order === 'desc')
400
- events.reverse();
401
- if (options.json)
402
- return events.map((item) => jsonLine(projection.sayNumbers, item)).join('\n') + (events.length > 0 ? '\n' : '');
403
- if (options.format !== undefined && options.format.length > 0) {
404
- return events.map((item) => renderFields(projection.sayNumbers, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
405
- }
406
- const pattern = options.grep ?? options.fixed;
407
- const output = pattern === undefined || pattern === ''
408
- ? renderHistoryProjection(projection, events, options.noTruncate === true, squarePath)
409
- : renderGrepActivitiesView(events, totalMatches, options.noTruncate, squarePath, pattern, options.fixed !== undefined, () => 'full');
410
- const publicEvents = events.filter(isArchiveActivity);
411
- const allPublic = projection.activities.filter(isArchiveActivity);
412
- const pageMin = publicEvents.length === 0 ? undefined : Math.min(...publicEvents.map((item) => item.index));
413
- const pageMax = publicEvents.length === 0 ? undefined : Math.max(...publicEvents.map((item) => item.index));
414
- const hasMore = options.lastN != null && publicEvents.length > 0 && (options.afterIndex !== undefined
415
- ? allPublic.some((item) => item.index > (pageMax ?? options.afterIndex))
416
- : allPublic.some((item) => item.index < (pageMin ?? Infinity)));
417
- const cursorDirection = options.afterIndex !== undefined ? '--after' : '--before';
418
- const cursorIndex = cursorDirection === '--after' ? Math.max(...publicEvents.map((item) => item.index)) : Math.min(...publicEvents.map((item) => item.index));
419
- const continuation = hasMore ? `\n\n${historyContinuationCommand(options, squarePath, [cursorDirection, actId(cursorIndex), '--limit', String(options.lastN ?? HISTORY_DEFAULT_LIMIT)])}` : '';
420
- return withPathOutput(squarePath, output + continuation, { participantCount: projection.participantCount });
449
+ outcome = await application(context).history(options.query);
421
450
  }
422
- finally {
423
- await closeOpenSquare(square);
451
+ catch (error) {
452
+ const bounds = error?.operation?.historyBounds;
453
+ if (bounds !== undefined)
454
+ fail(`✕ history is capped at ${bounds.maxLimit} activities\n${historyContinuationCommand(options, squarePath, ['--limit', String(bounds.maxLimit)])}`);
455
+ throw error;
424
456
  }
457
+ const projection = outcome.projection;
458
+ const events = projection.activities;
459
+ if (options.json)
460
+ return events.map((item) => jsonLine(projection.sayNumbers, item)).join('\n') + (events.length > 0 ? '\n' : '');
461
+ if (options.format !== undefined && options.format.length > 0) {
462
+ return events.map((item) => renderFields(projection.sayNumbers, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
463
+ }
464
+ const pattern = options.query.grep ?? options.query.fixed;
465
+ const output = pattern === undefined || pattern === ''
466
+ ? renderHistoryProjection(projection, projection.visibleActivities, options.noTruncate, squarePath)
467
+ : renderGrepActivitiesView(projection.visibleActivities, projection.totalMatches, options.noTruncate, squarePath, pattern, options.query.fixed !== undefined, () => 'full');
468
+ const next = projection.continuation;
469
+ const continuation = next === undefined ? '' : `\n\n${historyContinuationCommand(options, squarePath, [`--${next.direction}`, next.cursor, '--limit', String(next.limit)])}`;
470
+ return withPathOutput(squarePath, output + continuation, { participantCount: projection.standingCount });
425
471
  },
426
472
  present: (result) => process.stdout.write(result),
427
473
  };
@@ -442,30 +488,26 @@ export const participantsCommand = {
442
488
  parse(argv, context) { return parseParticipants(argv, context); },
443
489
  async execute(intent, context) {
444
490
  const squarePath = requireSquarePath(context);
445
- const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs });
446
- {
447
- const now = nowMs();
448
- const participants = await application.participants();
449
- const lines = participants.slice(0, intent.limit).map((participant) => {
450
- const recent = participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS;
451
- const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && recent ? '●' : '○';
452
- const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
453
- const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
454
- return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
455
- });
456
- const participantCount = participants.filter((participant) => participant.state === 'active').length;
457
- const tail = participants.length <= intent.limit
458
- ? []
459
- : [
460
- style('dim', ` ○ ${lines.length} of ${participants.length} participants shown`),
461
- ...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
462
- `${participantsLimitCommand(squarePath, participants.length)}`,
463
- ] : []),
464
- ];
465
- return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
466
- participantCount,
467
- });
468
- }
491
+ const { data, projection } = await application(context).participants();
492
+ const now = projection.now;
493
+ const participants = data.participants;
494
+ const lines = participants.slice(0, intent.limit).map((participant) => {
495
+ const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
496
+ const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
497
+ const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
498
+ return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
499
+ });
500
+ const tail = participants.length <= intent.limit
501
+ ? []
502
+ : [
503
+ style('dim', ` ○ ${lines.length} of ${participants.length} participants shown`),
504
+ ...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
505
+ `${participantsLimitCommand(squarePath, participants.length)}`,
506
+ ] : []),
507
+ ];
508
+ return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
509
+ participantCount: projection.standingCount,
510
+ });
469
511
  },
470
512
  present: (result) => process.stdout.write(result),
471
513
  };
@@ -474,100 +516,88 @@ export const statusCommand = {
474
516
  usage(context.command); return undefined; },
475
517
  async execute(_intent, context) {
476
518
  const squarePath = requireSquarePath(context);
477
- const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs });
478
- {
479
- const presentation = await application.status();
480
- const result = presentation.status;
481
- const active = result.participants.filter((participant) => participant.state === 'active');
482
- const here = active.filter((participant) => participant.presence === 'watching'
483
- || (participant.lastActiveAt !== undefined && result.now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS));
484
- const lingering = active.length - here.length;
485
- const orderedHere = [
486
- ...here.filter((participant) => participant.presence === 'watching'),
487
- ...here.filter((participant) => participant.presence !== 'watching'),
488
- ];
489
- const people = [];
490
- if (active.length === 0) {
491
- people.push(' ○ nobody in the square');
492
- }
493
- else {
494
- if (orderedHere.length === 0)
495
- people.push(' ○ nobody here right now');
496
- people.push(...orderedHere.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
497
- const glyph = participant.presence === 'watching'
498
- ? '◎'
499
- : participant.activityCount > 0 ? '●' : '○';
500
- const summary = participant.presence === 'watching'
501
- ? 'is nearby · catching'
502
- : participant.activityCount > 0
503
- ? `is nearby · ${formatRelativeTime(participant.lastActiveAt ?? result.now, result.now)}`
504
- : `is nearby · quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
505
- const showAttention = context.name === undefined || sameName(participant.name, context.name);
506
- const attention = !showAttention
507
- ? ''
508
- : participant.pendingMentionCount > 0
509
- ? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
510
- : participant.unreadActivityCount > 0
511
- ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
512
- : 'caught up';
513
- return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
514
- }));
515
- const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
516
- if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
517
- people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
518
- }
519
- if (lingering > 0)
520
- people.push(` ○ … ${lingering} lingering`);
521
- if (unshown > 0) {
522
- people.push(`${result.participants.length <= PARTICIPANTS_MAX_LIMIT
523
- ? participantsLimitCommand(squarePath, result.participants.length)
524
- : `${commandPrefix(squarePath)} participants`}`);
525
- }
526
- }
527
- const cap = result.hardCap === null ? 'unlimited' : `${result.hardCap} each`;
528
- const holdTime = result.holdAt === undefined ? 'just now' : formatRelativeTime(result.holdAt, result.now);
529
- const hold = result.holdActive
530
- ? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''}${style('dim', ` · ${holdTime}`)}`
531
- : undefined;
532
- const visible = result.latestAct === undefined
533
- ? { text: '', bodyClipped: false }
534
- : renderAmbientEventFacts(result.latestAct, context.name ?? '', {
535
- now: result.now,
536
- preview: 200,
537
- actNumber: presentation.latestActNumber,
538
- squareState: presentation.state,
539
- });
540
- const latest = visible.text === ''
541
- ? [result.latestAct === undefined
542
- ? ' ○ no public activity yet'
543
- : ' · the latest words were meant for other ears']
544
- : [` ${visible.text.replace(/\n/g, '\n ')}`];
545
- // Presence events after the latest public act: otherwise the roster can say someone is
546
- // here while the latest line shows them leaving, with nothing to reconcile the two.
547
- const latestIndex = result.latestAct?.index ?? -1;
548
- const stepsIn = presentation.state.acts.filter((act) => act.kind === 'join' && act.index > latestIndex);
549
- for (const act of stepsIn.slice(-3)) {
550
- const rejoined = presentation.state.acts.some((other) => other.kind === 'done' && other.actor === act.actor && other.index < act.index);
551
- latest.push(style('dim', ` → ${participantIdentity(act.actor)} ${rejoined ? 'stepped back in' : 'stepped in'} · ${formatRelativeTime(act.at, result.now)}`));
519
+ const { data: result, projection: presentation } = await application(context).status(context.name);
520
+ const participantsByName = new Map(result.participants.map((participant) => [participant.name, participant]));
521
+ const orderedHere = presentation.orderedHereNames.map((name) => participantsByName.get(name));
522
+ const lingering = presentation.lingeringCount;
523
+ const people = [];
524
+ if (result.activeCount === 0) {
525
+ people.push(' ○ nobody in the square');
526
+ }
527
+ else {
528
+ if (orderedHere.length === 0)
529
+ people.push(' ○ nobody here right now');
530
+ people.push(...orderedHere.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
531
+ const glyph = participant.presence === 'watching'
532
+ ? '◎'
533
+ : participant.activityCount > 0 ? '●' : '○';
534
+ const summary = participant.presence === 'watching'
535
+ ? 'is nearby · catching'
536
+ : participant.activityCount > 0
537
+ ? `is nearby · ${formatRelativeTime(participant.lastActiveAt ?? result.now, result.now)}`
538
+ : `is nearby · quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
539
+ const showAttention = presentation.attentionNames.includes(participant.name);
540
+ const attention = !showAttention
541
+ ? ''
542
+ : participant.pendingMentionCount > 0
543
+ ? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
544
+ : participant.unreadActivityCount > 0
545
+ ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
546
+ : 'caught up';
547
+ return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
548
+ }));
549
+ const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
550
+ if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
551
+ people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
552
552
  }
553
- if (stepsIn.length > 3)
554
- latest.push(style('dim', ` → … ${stepsIn.length - 3} more stepped in`));
555
- if (visible.bodyClipped && result.latestAct !== undefined) {
556
- latest.push(`${commandPrefix(squarePath)} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
553
+ if (lingering > 0)
554
+ people.push(` ○ … ${lingering} lingering`);
555
+ if (unshown > 0) {
556
+ people.push(`${presentation.knownParticipantCount <= PARTICIPANTS_MAX_LIMIT
557
+ ? participantsLimitCommand(squarePath, presentation.knownParticipantCount)
558
+ : `${commandPrefix(squarePath)} participants`}`);
557
559
  }
558
- const counts = [
559
- `${here.length} here`,
560
- ...(lingering > 0 ? [`${lingering} lingering`] : []),
561
- `${result.doneCount} done`,
562
- `cap ${cap}`,
563
- `throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
564
- ].join(' · ');
565
- const output = [
566
- style('dim', counts),
567
- ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
568
- ].join('\n');
569
- return withPathOutput(squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
570
560
  }
561
+ const cap = result.hardCap === null ? 'unlimited' : `${result.hardCap} each`;
562
+ const holdTime = result.hold?.at === undefined ? 'just now' : formatRelativeTime(result.hold.at, result.now);
563
+ const hold = result.hold !== null
564
+ ? `· ${result.hold.by === undefined ? 'someone' : participantIdentity(result.hold.by)} raised a hand${result.hold.reason ? ` — ${result.hold.reason}` : ''}${style('dim', ` · ${holdTime}`)}`
565
+ : undefined;
566
+ const visible = result.latestActivity === undefined
567
+ ? { text: '', bodyClipped: false }
568
+ : renderAmbientEventFacts(result.latestActivity, presentation.viewer, {
569
+ now: result.now,
570
+ preview: 200,
571
+ actNumber: presentation.latestActNumber,
572
+ perception: presentation.latestPerception,
573
+ });
574
+ const latest = visible.text === ''
575
+ ? [result.latestActivity === undefined
576
+ ? ' ○ no public activity yet'
577
+ : ' · the latest words were meant for other ears']
578
+ : [` ${visible.text.replace(/\n/g, '\n ')}`];
579
+ // Presence events after the latest public act: otherwise the roster can say someone is
580
+ // here while the latest line shows them leaving, with nothing to reconcile the two.
581
+ for (const { activity, rejoined } of presentation.joins) {
582
+ latest.push(style('dim', ` → ${participantIdentity(activity.actor)} ${rejoined ? 'stepped back in' : 'stepped in'} · ${formatRelativeTime(activity.at, result.now)}`));
583
+ }
584
+ if (presentation.joinCount > 3)
585
+ latest.push(style('dim', ` → … ${presentation.joinCount - 3} more stepped in`));
586
+ if (visible.bodyClipped && result.latestActivity !== undefined) {
587
+ latest.push(`${commandPrefix(squarePath)} history --at ${result.latestActivity.id} -C 2 --no-truncate`);
588
+ }
589
+ const counts = [
590
+ `${orderedHere.length} here`,
591
+ ...(lingering > 0 ? [`${lingering} lingering`] : []),
592
+ `${result.doneCount} done`,
593
+ `cap ${cap}`,
594
+ `throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
595
+ ].join(' · ');
596
+ const output = [
597
+ style('dim', counts),
598
+ ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
599
+ ].join('\n');
600
+ return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: presentation.held });
571
601
  },
572
602
  present: (result) => process.stdout.write(result),
573
603
  };
@@ -608,35 +638,18 @@ export const inboxCommand = {
608
638
  : INBOX_DEFAULT_LIMIT;
609
639
  return { sessionId, json, limit };
610
640
  },
611
- async execute(intent) {
612
- const inbox = await sessionInbox(intent.sessionId);
613
- const ordered = [...inbox].sort((left, right) => {
614
- if (left.squarePath < right.squarePath)
615
- return -1;
616
- if (left.squarePath > right.squarePath)
617
- return 1;
618
- const leftName = nameKey(left.name);
619
- const rightName = nameKey(right.name);
620
- if (leftName < rightName)
621
- return -1;
622
- if (leftName > rightName)
623
- return 1;
624
- if (left.name < right.name)
625
- return -1;
626
- if (left.name > right.name)
627
- return 1;
628
- return 0;
629
- });
630
- const rows = ordered.slice(0, intent.limit).map((membership) => ({
641
+ async execute(intent, context) {
642
+ const { data } = await application(context).inbox(intent.sessionId, { limit: intent.limit });
643
+ const rows = data.rows.map((membership) => ({
631
644
  namePreview: inboxDisplay(membership.name),
632
645
  squarePathPreview: inboxDisplay(membership.squarePath),
633
- pending: membership.notifications.length,
646
+ pending: membership.pending,
634
647
  }));
635
648
  if (intent.json)
636
- return `${JSON.stringify({ rows, total: ordered.length })}\n`;
649
+ return `${JSON.stringify({ rows, total: data.total })}\n`;
637
650
  return [
638
651
  ...rows.map((row) => `${row.namePreview}\t${row.squarePathPreview}\t${row.pending}\n`),
639
- ...(rows.length < ordered.length ? [`${rows.length} of ${ordered.length} memberships shown\n`] : []),
652
+ ...(rows.length < data.total ? [`${rows.length} of ${data.total} memberships shown\n`] : []),
640
653
  ].join('');
641
654
  },
642
655
  present: (result) => process.stdout.write(result),