@astrosheep/square 0.4.7 → 0.4.8

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 (83) hide show
  1. package/dist/card-format.d.ts +2 -0
  2. package/dist/card-format.js +2 -0
  3. package/dist/claude-mcp-context.d.ts +0 -1
  4. package/dist/claude-mcp-context.js +0 -10
  5. package/dist/claude-mod.d.ts +2 -0
  6. package/dist/claude-mod.js +211 -0
  7. package/dist/cli/context.d.ts +1 -1
  8. package/dist/cli/context.js +1 -12
  9. package/dist/cli/express-presentation.js +6 -33
  10. package/dist/cli/json.js +1 -1
  11. package/dist/cli/observation-commands.d.ts +2 -3
  12. package/dist/cli/observation-commands.js +21 -59
  13. package/dist/cli/registry.js +3 -5
  14. package/dist/cli/square-commands.d.ts +1 -2
  15. package/dist/cli/square-commands.js +7 -17
  16. package/dist/decisions.js +5 -4
  17. package/dist/harness-claude.js +3 -3
  18. package/dist/help.js +6 -8
  19. package/dist/mcp.js +1 -1
  20. package/dist/neutral.d.ts +15 -1
  21. package/dist/notifications.js +1 -1
  22. package/dist/operation-presentation.d.ts +17 -15
  23. package/dist/operation-presentation.js +58 -32
  24. package/dist/packages/agent-delivery/src/index.d.ts +4 -9
  25. package/dist/packages/agent-delivery/src/index.js +0 -9
  26. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +2 -12
  27. package/dist/packages/agent-delivery/src/local-endpoint.js +4 -48
  28. package/dist/packages/agent-delivery/src/pi-client.js +0 -2
  29. package/dist/packages/agent-delivery/src/types.d.ts +3 -37
  30. package/dist/plugins/paseo/shared/card-format.d.ts +6 -0
  31. package/dist/plugins/paseo/shared/card-format.js +38 -0
  32. package/dist/plugins/paseo/shared/card-presentation.d.ts +3 -1
  33. package/dist/plugins/paseo/shared/card-presentation.js +3 -1
  34. package/dist/presentation.d.ts +2 -20
  35. package/dist/presentation.js +11 -46
  36. package/dist/sqlite-square.js +5 -6
  37. package/dist/square-facade.d.ts +3 -12
  38. package/dist/square-posting.js +1 -1
  39. package/dist/square-tools.d.ts +0 -11
  40. package/dist/square-tools.js +33 -119
  41. package/dist/square-values.d.ts +1 -1
  42. package/dist/square-values.js +1 -1
  43. package/dist/time.d.ts +1 -0
  44. package/dist/time.js +10 -0
  45. package/dist/views.d.ts +1 -1
  46. package/dist/views.js +2 -2
  47. package/dist/wake-transports.d.ts +1 -1
  48. package/dist/wake-transports.js +1 -1
  49. package/guides/catch-cold-path-optimization.md +1 -1
  50. package/guides/claude-native.md +37 -7
  51. package/guides/delivery-primitives.md +2 -1
  52. package/guides/mcp.md +3 -3
  53. package/guides/pi-native-delivery.md +10 -2
  54. package/guides/pi-tools.md +1 -3
  55. package/guides/row-storage-design.md +1 -1
  56. package/package.json +1 -1
  57. package/plugins/claude/.claude-plugin/plugin.json +3 -2
  58. package/plugins/claude/README.md +9 -0
  59. package/plugins/claude/hooks/hooks.json +1 -9
  60. package/plugins/claude/hooks/register.js +178 -0
  61. package/plugins/claude/skills/square/SKILL.md +2 -2
  62. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  63. package/plugins/paseo/OVERVIEW.md +1 -1
  64. package/plugins/paseo/README.md +20 -23
  65. package/plugins/paseo/client/markdown.tsx +88 -95
  66. package/plugins/paseo/client/palette.ts +37 -12
  67. package/plugins/paseo/client/square-message.tsx +52 -76
  68. package/plugins/paseo/client/transform.ts +4 -6
  69. package/plugins/paseo/shared/card-format.ts +37 -0
  70. package/plugins/paseo/shared/card-presentation.ts +6 -2
  71. package/plugins/paseo/shared/message.ts +1 -6
  72. package/plugins/pi/square-pi.js +69 -50
  73. package/skills/square/SKILL.md +2 -2
  74. package/dist/claude-boundary-state.d.ts +0 -20
  75. package/dist/claude-boundary-state.js +0 -84
  76. package/dist/claude-hook.d.ts +0 -2
  77. package/dist/claude-hook.js +0 -232
  78. package/dist/packages/agent-delivery/src/claude-auth.d.ts +0 -13
  79. package/dist/packages/agent-delivery/src/claude-auth.js +0 -75
  80. package/dist/packages/agent-delivery/src/claude-native.d.ts +0 -30
  81. package/dist/packages/agent-delivery/src/claude-native.js +0 -75
  82. package/dist/packages/agent-delivery/src/claude.d.ts +0 -5
  83. package/dist/packages/agent-delivery/src/claude.js +0 -66
@@ -0,0 +1,38 @@
1
+ export function reachLabel(reach) {
2
+ return reach === "mention" ? "@you" : reach === "attention" ? undefined : reach;
3
+ }
4
+ /** Calendar days, rather than elapsed hours, keep yesterday correct across DST. */
5
+ export function timeLabel(at, now = Date.now(), compact = false) {
6
+ const date = new Date(at), today = new Date(now);
7
+ const day = (value) => `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}`;
8
+ const clock = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
9
+ if (day(date) === day(today))
10
+ return clock;
11
+ const yesterday = new Date(today);
12
+ yesterday.setDate(today.getDate() - 1);
13
+ if (day(date) === day(yesterday))
14
+ return `${compact ? "yest" : "yesterday"} ${clock}`;
15
+ const shortDate = compact
16
+ ? `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`
17
+ : date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
18
+ return `${shortDate} ${clock}`;
19
+ }
20
+ /** Only adjacent references from the same origin can form a range. */
21
+ export function cardReferences(items, originLabel = origin => origin) {
22
+ const multipleOrigins = new Set(items.map(item => item.origin)).size > 1;
23
+ const runs = [];
24
+ for (const item of items) {
25
+ if (item.ref === undefined)
26
+ continue;
27
+ const match = /^act\/(\d+)$/.exec(item.ref);
28
+ const index = match ? Number(match[1]) : undefined;
29
+ const previous = runs.at(-1);
30
+ if (index !== undefined && previous?.origin === item.origin && previous.last !== undefined && previous.last + 1 === index) {
31
+ previous.last = index;
32
+ }
33
+ else
34
+ runs.push({ origin: item.origin, ref: item.ref, first: index, last: index });
35
+ }
36
+ return runs.map(run => `${multipleOrigins ? `${originLabel(run.origin)} ` : ""}${run.first !== run.last
37
+ ? `act/${run.first}–${run.last}` : run.ref}`).join(", ");
38
+ }
@@ -4,7 +4,9 @@ export type CardPresentation = {
4
4
  readonly origin: string;
5
5
  readonly author?: string;
6
6
  readonly ref?: string;
7
- readonly badge?: string;
7
+ /** Absent on cards produced before landing time and reach were recorded. */
8
+ readonly at?: number;
9
+ readonly reach?: 'mention' | 'reply' | 'attention' | 'bell';
8
10
  readonly body: string;
9
11
  };
10
12
  export declare function isCardPresentation(value: unknown): value is CardPresentation;
@@ -3,5 +3,7 @@ export function isCardPresentation(value) {
3
3
  return false;
4
4
  const item = value;
5
5
  return item.kind === 'presentation' && typeof item.origin === 'string' && typeof item.body === 'string'
6
- && ['author', 'ref', 'badge'].every(key => item[key] === undefined || typeof item[key] === 'string');
6
+ && (item.at === undefined || (typeof item.at === 'number' && Number.isFinite(item.at)))
7
+ && (item.reach === undefined || ['mention', 'reply', 'attention', 'bell'].includes(item.reach))
8
+ && ['author', 'ref'].every(key => item[key] === undefined || typeof item[key] === 'string');
7
9
  }
@@ -17,26 +17,10 @@ interface ParticipantOutputOptions {
17
17
  name: string;
18
18
  participantCount?: number;
19
19
  }
20
- interface ActivityLimitOptions extends ParticipantOutputOptions {
21
- count?: number;
22
- hardCap?: number;
23
- draftPath?: string;
24
- }
25
- interface ActivityBlockedOptions extends ParticipantOutputOptions {
26
- retryCommand: string;
27
- activitySummaries: readonly UnreadActivitySummary[];
28
- draftPath?: string;
29
- }
30
20
  interface ExpressWaitingOptions {
31
21
  reason: 'throttled';
32
22
  delayMs?: number;
33
23
  }
34
- interface ExpressNoWaitOptions extends ParticipantOutputOptions {
35
- reason: 'throttled';
36
- retryCommand: string;
37
- delayMs?: number;
38
- draftPath?: string;
39
- }
40
24
  interface WatchStatusOptions extends ParticipantOutputOptions {
41
25
  status: WatchStatus;
42
26
  idleMs?: number;
@@ -92,7 +76,7 @@ export declare function renderAmbientEvent(event: HydratedAct | Activity, viewer
92
76
  mention?: string;
93
77
  perception?: Perception;
94
78
  }): string;
95
- export declare function renderActivityBlocked(opts: ActivityBlockedOptions): string;
79
+ export declare function renderUnreadSummary(activitySummaries: readonly UnreadActivitySummary[]): string[];
96
80
  /** An indeterminate or already-committed send only points to inspection, never resend. */
97
81
  export declare function renderActivityUncertain(opts: ParticipantOutputOptions & {
98
82
  draftPath: string;
@@ -100,10 +84,8 @@ export declare function renderActivityUncertain(opts: ParticipantOutputOptions &
100
84
  detail?: string;
101
85
  }): string;
102
86
  export declare function renderExpressWaiting(opts: ExpressWaitingOptions): string;
103
- export declare function renderExpressNoWait(opts: ExpressNoWaitOptions): string;
104
87
  export declare function renderPresenceAnchor(names: readonly string[]): string;
105
- export declare function renderGrepActivitiesView(visible: readonly HydratedAct[], totalMatches: number, noTruncate: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean, perception?: (act: HydratedAct) => Perception, surface?: PresentationSurface): string;
106
- export declare function renderActivityLimit(opts: ActivityLimitOptions): string;
88
+ export declare function renderGrepActivitiesView(visible: readonly HydratedAct[], totalMatches: number, full: boolean | undefined, squarePath: string, pattern: string, fixed?: boolean, perception?: (act: HydratedAct) => Perception, surface?: PresentationSurface): string;
107
89
  export declare function renderWatchAlreadyActive(opts: ParticipantOutputOptions): string;
108
90
  export declare function renderWatchForceTakeover(_opts: ParticipantOutputOptions): string;
109
91
  export declare function renderWatchReplaceMissing(_opts: ParticipantOutputOptions): string;
@@ -214,17 +214,14 @@ export function renderAmbientEvent(event, viewer, opts = {}) {
214
214
  function draftSavedLines(draftPath) {
215
215
  return draftPath === undefined ? [] : [style('dim', `· draft kept: ${draftPath}`)];
216
216
  }
217
- function withDraftInput(command, draftPath) {
218
- return draftPath === undefined ? command : `${command} < ${quoteShell(draftPath)}`;
219
- }
220
- function renderUnreadSummary(activitySummaries) {
217
+ export function renderUnreadSummary(activitySummaries) {
221
218
  const visibleSummaries = activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
222
219
  const remainingSummaries = activitySummaries.length - visibleSummaries.length;
223
220
  return [
224
221
  ...visibleSummaries.map((item) => {
225
222
  const compact = (item.preview.body ?? '').replace(/\s+/g, ' ').trim();
226
223
  const bodyPreview = compact === ''
227
- ? { text: item.preview.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
224
+ ? { text: item.preview.structured ? '(structured activity)' : '(empty)', remaining: 0 }
228
225
  : truncateChars(compact, UNREAD_PREVIEW_CHARS);
229
226
  const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
230
227
  return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
@@ -232,17 +229,6 @@ function renderUnreadSummary(activitySummaries) {
232
229
  ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
233
230
  ];
234
231
  }
235
- export function renderActivityBlocked(opts) {
236
- const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} perceive`;
237
- return withPathOutput(opts.squarePath, [
238
- `${style('blocked', '✕')} your activity hasn't landed — someone spoke while you were away`,
239
- ...renderUnreadSummary(opts.activitySummaries),
240
- ...draftSavedLines(opts.draftPath),
241
- `${readNowCommand}`,
242
- ' · take it in, then retry:',
243
- withDraftInput(opts.retryCommand, opts.draftPath),
244
- ].join('\n'), { participantCount: opts.participantCount });
245
- }
246
232
  /** An indeterminate or already-committed send only points to inspection, never resend. */
247
233
  export function renderActivityUncertain(opts) {
248
234
  return formatRefusal(opts.squarePath, [
@@ -252,7 +238,7 @@ export function renderActivityUncertain(opts) {
252
238
  ...(opts.detail === undefined ? [] : [` · ${truncateExternalDiagnostic(opts.detail)}`]),
253
239
  ...draftSavedLines(opts.draftPath),
254
240
  ' · the draft is a saved copy; sending it again could repeat your activity',
255
- `${commandPrefix(opts.squarePath)} history${opts.landedId === undefined ? ' --limit 10' : ` --at ${opts.landedId}`} --no-truncate`,
241
+ `${commandPrefix(opts.squarePath)} history${opts.landedId === undefined ? ' --limit 10' : ` --at ${opts.landedId}`} --full`,
256
242
  ], opts);
257
243
  }
258
244
  export function renderExpressWaiting(opts) {
@@ -262,16 +248,6 @@ export function renderExpressWaiting(opts) {
262
248
  ' · --no-wait saves a draft and returns now',
263
249
  ].join('\n');
264
250
  }
265
- export function renderExpressNoWait(opts) {
266
- const retryCommand = opts.retryCommand;
267
- const lines = [
268
- `${style('blocked', '✕')} the square is packed — shoulder to shoulder`,
269
- ` · a lull opens in ${formatDuration(opts.delayMs)}`,
270
- ...draftSavedLines(opts.draftPath),
271
- `${withDraftInput(retryCommand, opts.draftPath)}`,
272
- ];
273
- return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount });
274
- }
275
251
  export function renderPresenceAnchor(names) {
276
252
  const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
277
253
  const participants = visibleNames.map((name) => participantIdentity(name)).join(', ');
@@ -280,7 +256,7 @@ export function renderPresenceAnchor(names) {
280
256
  return style('dim', names.length === 1 ? `→ ${participants} was here` : `→ ${participants}${suffix} were here`);
281
257
  }
282
258
  const GREP_PREVIEW_CHARS = 160;
283
- export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squarePath, pattern, fixed = false, perception, surface = 'cli') {
259
+ export function renderGrepActivitiesView(visible, totalMatches, full, squarePath, pattern, fixed = false, perception, surface = 'cli') {
284
260
  const publicVisible = visible.filter((act) => act.kind === 'say' || act.kind === 'done');
285
261
  if (totalMatches === 0)
286
262
  return `○ no activity matched ${quoteShell(pattern)}`;
@@ -294,7 +270,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
294
270
  chunks.push(header);
295
271
  continue;
296
272
  }
297
- if (noTruncate === true) {
273
+ if (full === true) {
298
274
  const body = rawBody.split('\n').map((line) => ` ${line}`).join('\n');
299
275
  chunks.push(`${header}\n${body}`);
300
276
  continue;
@@ -316,34 +292,23 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
316
292
  }
317
293
  if (publicVisible.length === 1) {
318
294
  chunks.push(surface === 'tool'
319
- ? formatToolAction('history', { location: displayPath(squarePath), at: [formatActivityId(publicVisible[0].index)], context: 2, ...(truncated ? { noTruncate: true } : {}) })
320
- : `${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2${truncated ? ' --no-truncate' : ''}`);
295
+ ? formatToolAction('history', { location: displayPath(squarePath), at: [formatActivityId(publicVisible[0].index)], context: 2, ...(truncated ? { full: true } : {}) })
296
+ : `${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2${truncated ? ' --full' : ''}`);
321
297
  }
322
298
  else if (truncated && publicVisible.length > 1) {
323
299
  chunks.push(surface === 'tool'
324
- ? formatToolAction('history', { location: displayPath(squarePath), at: [formatActivityId(publicVisible[0].index)], context: 2, noTruncate: true })
325
- : `${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2 --no-truncate`);
300
+ ? formatToolAction('history', { location: displayPath(squarePath), at: [formatActivityId(publicVisible[0].index)], context: 2, full: true })
301
+ : `${commandPrefix(squarePath)} history --at ${formatActivityId(publicVisible[0].index)} -C 2 --full`);
326
302
  }
327
303
  return chunks.join('\n\n');
328
304
  }
329
- export function renderActivityLimit(opts) {
330
- // The cap wording lives inside the count: the speaker is simply spent.
331
- const spoken = opts.count !== undefined && opts.hardCap !== undefined ? ` — ${opts.count}/${opts.hardCap} spoken` : '';
332
- const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`;
333
- return withPathOutput(opts.squarePath, [
334
- `${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`,
335
- ...draftSavedLines(opts.draftPath),
336
- ' · your draft stays unsent; done only steps out',
337
- doneCommand,
338
- ].join('\n'), { participantCount: opts.participantCount });
339
- }
340
305
  export function renderWatchAlreadyActive(opts) {
341
306
  return [
342
307
  `${style('blocked', '✕')} you are already perceiving`,
343
308
  ` · an active perceive is already running for ${participantIdentity(opts.name)}`,
344
309
  ` · ${opts.surface === 'tool' ? 'takeover' : '--takeover'} lets a new perceive take over`,
345
310
  opts.surface === 'tool'
346
- ? formatToolAction('perceive', { location: displayPath(opts.squarePath), as: opts.name, idle: 1800000, takeover: true })
311
+ ? formatToolAction('perceive', { location: displayPath(opts.squarePath), as: opts.name, idle: '30m', takeover: true })
347
312
  : `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} perceive --idle 30m --takeover`,
348
313
  ].join('\n');
349
314
  }
@@ -375,7 +340,7 @@ export function renderWatchStatus(opts) {
375
340
  ? style('dim', `○ dust settles on the flagstones — ${formatDuration(idleMs)} of quiet`)
376
341
  : style('dim', '○ only footsteps in the square — nothing new for you');
377
342
  // The quiet report never repeats the perceive just run.
378
- const hints = [opts.surface === 'tool' ? formatToolAction('perceive', { location: displayPath(opts.squarePath), as: opts.name, idle: 1800000 }) : `${prefix} perceive --idle 30m`];
343
+ const hints = [opts.surface === 'tool' ? formatToolAction('perceive', { location: displayPath(opts.squarePath), as: opts.name, idle: '30m' }) : `${prefix} perceive --idle 30m`];
379
344
  return [
380
345
  quiet,
381
346
  ...(opts.showPerceiveHint === false ? [] : hints),
@@ -215,9 +215,9 @@ export class SqliteSquare {
215
215
  ...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
216
216
  const decision = rejectUnsentActivity(() => {
217
217
  if (proposed.type === 'blocked')
218
- throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: proposed.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
218
+ throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { activitySummaries: proposed.activitySummaries });
219
219
  if (proposed.type === 'capped')
220
- throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
220
+ throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`, { spoken: facts.participant(name).sayCount, ...(facts.header().hardCap == null ? {} : { hardCap: facts.header().hardCap }) });
221
221
  if (proposed.type === 'throttled')
222
222
  throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: proposed.delayMs });
223
223
  if (proposed.type === 'bell_quota')
@@ -515,11 +515,10 @@ export class SqliteSquare {
515
515
  throw new SquareError('invalid_args', 'History order must be asc or desc.');
516
516
  if (query.since !== undefined && !Number.isFinite(query.since))
517
517
  throw new SquareError('invalid_args', 'History timestamp must be finite');
518
- for (const count of [query.contextBefore, query.contextAfter])
519
- if (count !== undefined && (!Number.isSafeInteger(count) || count < 0))
520
- throw new SquareError('invalid_args', 'History context must be a non-negative safe integer');
518
+ if (query.context !== undefined && (!Number.isSafeInteger(query.context) || query.context < 0 || query.context > HISTORY_MAX_LIMIT))
519
+ throw new SquareError('invalid_args', `History context must be a non-negative integer no greater than ${HISTORY_MAX_LIMIT}`);
521
520
  const limit = query.limit === undefined ? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT) : query.limit;
522
- const contextual = !!query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined;
521
+ const contextual = !!query.at?.length || query.context !== undefined;
523
522
  const kinds = contextual || query.since !== undefined ? [...NON_READ, 'read'] : NON_READ;
524
523
  return (await this.artifact.read(facts => {
525
524
  const selected = selectHistory(facts, query, limit, kinds, query.observe === true);
@@ -2,7 +2,7 @@ import type { MetadataMatch } from './delivery-deferral.js';
2
2
  export type { MetadataMatch, MetadataScalar } from './delivery-deferral.js';
3
3
  import type { ActivityId, Perception, Reach } from './square-core.js';
4
4
  import type { Owner, HydratedAct, WakeAddress, ActivityMetadata, DirectedNotificationRoute } from './model.js';
5
- import type { OperationControl } from './neutral.js';
5
+ import type { OperationControl, UnreadActivitySummary } from './neutral.js';
6
6
  export interface ActivitySearch {
7
7
  readonly text: string;
8
8
  readonly mode: 'literal' | 'regex';
@@ -16,7 +16,7 @@ export interface HeldSeat {
16
16
  readonly name: string;
17
17
  readonly generation: string;
18
18
  }
19
- export type { PerceiveWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError } from './neutral.js';
19
+ export type { PerceiveWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError, UnreadActivitySummary, SquareErrorFacts } from './neutral.js';
20
20
  export type { ActivityId, Perception, Reach } from './square-core.js';
21
21
  export type { Owner, WakeAddress, ActivityMetadata, JsonValue, DirectedNotificationRoute } from './model.js';
22
22
  /** Indexed archive event, including read bookkeeping and a say's linked content. */
@@ -119,8 +119,7 @@ export interface HistoryQuery extends ActivityQuery, ObservationOptions {
119
119
  readonly order?: 'asc' | 'desc';
120
120
  readonly mention?: string;
121
121
  readonly since?: number;
122
- readonly contextBefore?: number;
123
- readonly contextAfter?: number;
122
+ readonly context?: number;
124
123
  }
125
124
  export interface HistoryResult {
126
125
  readonly activities: readonly Activity[];
@@ -255,14 +254,6 @@ export interface StreamOptions {
255
254
  readonly start?: StreamStart;
256
255
  }
257
256
  export type StreamRecord = Activity | PerceivedActivity;
258
- export interface UnreadActivitySummary {
259
- readonly name: string;
260
- readonly count: number;
261
- readonly latestActivityAgeMs: number;
262
- readonly preview: Extract<ArchiveRecord, {
263
- kind: 'say';
264
- }>;
265
- }
266
257
  export interface EntryPresentation {
267
258
  readonly joinedBefore: boolean;
268
259
  readonly scene: string;
@@ -134,7 +134,7 @@ export async function flushSquarePosts(artifact, location, clock, signal, ledger
134
134
  const result = await ledger.post({ owner: post.owner, key: squarePostKey(location, post), text: rendered.text,
135
135
  ...(post.kind === 'activity' ? { details: { kind: 'presentation', origin: displayAttentionPath(location),
136
136
  ref: formatActivityId(post.index), author: post.from, body: post.body ?? '',
137
- ...(post.route === 'bell' ? { badge: '🔔' } : {}) } } : {}),
137
+ at: post.at, reach: post.route } } : {}),
138
138
  }, signal);
139
139
  posted = ['posted', 'duplicate', 'suppressed'].includes(result.status);
140
140
  knownUnsent = !posted;
@@ -6,16 +6,6 @@ export interface ToolIdentity {
6
6
  readonly wakeAddress?: WakeAddress;
7
7
  }
8
8
  type JsonObject = Record<string, unknown>;
9
- export declare class SquareExpressDrafts {
10
- private readonly drafts;
11
- private purgeExpired;
12
- resolve(args: JsonObject, owner?: Owner): {
13
- args: JsonObject;
14
- token?: string;
15
- };
16
- consume(token?: string): void;
17
- retain(args: JsonObject, owner: Owner | undefined, token: string | undefined, failure: JsonObject, expressAttempted: boolean): string | undefined;
18
- }
19
9
  interface ToolBinding {
20
10
  identity: ToolIdentity;
21
11
  readonly now?: () => number;
@@ -48,6 +38,5 @@ export declare function squareToolFailure(error: unknown, signal?: AbortSignal):
48
38
  export declare function squareToolFailureText(failure: JsonObject, context?: {
49
39
  location?: string;
50
40
  participant?: string;
51
- draft?: string;
52
41
  }): string;
53
42
  export {};
@@ -1,95 +1,32 @@
1
- import crypto from 'node:crypto';
2
1
  import { z } from 'zod/v4';
3
2
  import { isSquareError, SquareError } from './model.js';
4
- import { requireOwner, requireJoinName, selectOwnSeat, HISTORY_MAX_LIMIT, PERCEIVE_DEFAULT_LIMIT, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_DEPTH, MAX_LIST_LIMIT } from './square-api.js';
5
- import { parseTimeOrRelative } from './time.js';
3
+ import { requireOwner, requireJoinName, selectOwnSeat, HISTORY_MAX_LIMIT, PERCEIVE_MAX_LIMIT, PERCEIVE_DEFAULT_LIMIT, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_DEPTH, MAX_LIST_LIMIT } from './square-api.js';
4
+ import { parseDurationMs, parseTimeOrRelative } from './time.js';
6
5
  import { renderPerceiveOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, renderSquareList, } from './operation-presentation.js';
7
- /** Process-local recovery: FIFO 64, fixed two-hour TTL, consumed only by a successful express. */
8
- const EXPRESS_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
9
- const EXPRESS_DRAFT_LIMIT = 64;
10
- export class SquareExpressDrafts {
11
- drafts = new Map();
12
- purgeExpired() {
13
- const now = Date.now();
14
- for (const [token, draft] of this.drafts)
15
- if (draft.expiresAt <= now)
16
- this.drafts.delete(token);
17
- }
18
- resolve(args, owner) {
19
- if (args.draft === undefined)
20
- return { args };
21
- this.purgeExpired();
22
- if (typeof args.draft !== 'string' || Object.keys(args).length !== 1)
23
- throw new SquareError('invalid_args', 'An express draft token cannot be combined with other arguments.');
24
- const draft = this.drafts.get(args.draft);
25
- if (draft === undefined || draft.owner !== owner)
26
- throw new SquareError('invalid_args', 'Unknown, expired, or inaccessible express draft.');
27
- return { args: draft.args, token: args.draft };
28
- }
29
- consume(token) {
30
- if (token !== undefined)
31
- this.drafts.delete(token);
32
- }
33
- retain(args, owner, token, failure, expressAttempted) {
34
- const effect = failure.operation?.expressEffect;
35
- // After an attempt, only an admission or argument refusal proves that retrying cannot duplicate an activity.
36
- const unsent = !expressAttempted || effect === 'unsent' || (effect === undefined && failure.code === 'invalid_args');
37
- if (!unsent || effect === 'landed' || effect === 'unknown' || typeof args.body !== 'string' || args.draft !== undefined
38
- || typeof args.location !== 'string' || !args.location.trim())
39
- return undefined;
40
- this.purgeExpired();
41
- if (token !== undefined && this.drafts.has(token))
42
- return token;
43
- const created = `dr_${crypto.randomBytes(16).toString('base64url')}`;
44
- this.drafts.set(created, { owner, args, expiresAt: Date.now() + EXPRESS_DRAFT_TTL_MS });
45
- if (this.drafts.size > EXPRESS_DRAFT_LIMIT)
46
- this.drafts.delete(this.drafts.keys().next().value);
47
- return created;
48
- }
49
- }
50
6
  export function createSquareToolRunner() {
51
- const drafts = new SquareExpressDrafts();
52
7
  return async (tool, args, control, bind) => {
53
- let toolArgs = args;
54
- let owner;
55
- let token;
56
- let attempted = false;
57
8
  let square;
58
9
  try {
59
10
  const binding = await bind();
60
- owner = binding.identity.owner;
61
11
  if ('runDiscovery' in tool)
62
- return { text: await tool.runDiscovery(toolArgs, control, binding) };
63
- if (tool.name === 'express') {
64
- const resolved = drafts.resolve(toolArgs, owner);
65
- toolArgs = resolved.args;
66
- token = resolved.token;
67
- }
68
- square = await binding.open(toolArgs.location, control);
69
- let text;
12
+ return { text: await tool.runDiscovery(args, control, binding) };
13
+ square = await binding.open(args.location, control);
70
14
  try {
71
- attempted = tool.name === 'express';
72
- text = await tool.run(square, toolArgs, control, binding.identity, binding.now);
73
- drafts.consume(token);
15
+ return { text: await tool.run(square, args, control, binding.identity, binding.now) };
74
16
  }
75
17
  finally {
76
18
  await square.close();
77
19
  }
78
- return { text };
79
20
  }
80
21
  catch (error) {
81
- const failure = squareToolFailure(error, control.signal);
82
- const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, token, failure, attempted) : undefined;
83
- return { isError: true, text: squareToolFailureText(failure, {
84
- location: square?.location ?? toolArgs.location,
85
- participant: typeof toolArgs.as === 'string' ? toolArgs.as : undefined,
86
- draft,
22
+ return { isError: true, text: squareToolFailureText(squareToolFailure(error, control.signal), {
23
+ location: square?.location ?? args.location,
24
+ participant: typeof args.as === 'string' ? args.as : undefined,
87
25
  }) };
88
26
  }
89
27
  };
90
28
  }
91
- const location = z.string().describe('Which square: a .square file path. Relative paths start from the working directory.');
92
- const expressLocation = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Required unless retrying with a draft token.');
29
+ const location = z.string().describe('Path to the .square file, relative to the working directory.');
93
30
  const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
94
31
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
95
32
  const stringArray = z.array(z.string());
@@ -98,7 +35,7 @@ function rendering(render) {
98
35
  return (result, args) => render(result, args);
99
36
  }
100
37
  function tool(name, description, shape, run, render) {
101
- const inputSchema = z.object({ ...shape, location: name === 'express' ? expressLocation : location }).strict();
38
+ const inputSchema = z.object({ ...shape, location }).strict();
102
39
  return { name, description, inputSchema,
103
40
  run: async (square, args, control, identity, now) => {
104
41
  const { location: _location, ...operationArgs } = inputSchema.parse(args);
@@ -115,21 +52,15 @@ export function squareToolsEnabled(env = process.env) {
115
52
  throw new SquareError('invalid_args', 'Invalid SQUARE_TOOLS: expected 0, 1, true, false, or none.');
116
53
  }
117
54
  export const SQUARE_TOOL_INSTRUCTIONS = 'List nearby squares and your joined names with list. Pass location for a single square. Join once. Then express to speak, and perceive to take in what reached you. History looks back without taking anything in.';
118
- function historyToolQuery({ grep, fixed, before, after, since, context, contextBefore, contextAfter, noTruncate: _noTruncate, ...query }, now = Date.now()) {
119
- if (grep !== undefined && fixed !== undefined)
120
- throw new SquareError('invalid_args', 'Use grep or fixed, not both.');
55
+ function historyToolQuery({ search, regex, before, after, since, full: _full, ...query }, now = Date.now()) {
121
56
  if (before !== undefined && after !== undefined)
122
57
  throw new SquareError('invalid_args', 'Use before or after, not both.');
123
58
  const sinceMs = since === undefined ? undefined : parseTimeOrRelative(since, now);
124
59
  if (sinceMs !== undefined && !Number.isFinite(sinceMs))
125
60
  throw new SquareError('invalid_args', `Invalid since timestamp: ${since}`);
126
- const resolvedBefore = contextBefore ?? context;
127
- const resolvedAfter = contextAfter ?? context;
128
61
  return { ...query,
129
62
  ...(sinceMs === undefined ? {} : { since: sinceMs }),
130
- ...(resolvedBefore === undefined ? {} : { contextBefore: resolvedBefore }),
131
- ...(resolvedAfter === undefined ? {} : { contextAfter: resolvedAfter }),
132
- search: grep !== undefined ? { text: grep, mode: 'regex' } : fixed === undefined ? undefined : { text: fixed, mode: 'literal' },
63
+ search: search === undefined ? undefined : { text: search, mode: regex ? 'regex' : 'literal' },
133
64
  cursor: after !== undefined ? { id: after, direction: 'after' } : before === undefined ? undefined : { id: before, direction: 'before' },
134
65
  };
135
66
  }
@@ -150,66 +81,57 @@ export const SQUARE_TOOLS = [
150
81
  },
151
82
  },
152
83
  tool('join', 'Step into the square under your name. Do this once before you speak or perceive. You arrive seeing what was said most recently.', {
153
- as: z.string().optional().describe('Your name in the square. Give it explicitly when joining.'),
84
+ as: z.string().describe('Your name in the square.'),
154
85
  takeover: z.boolean().optional().describe('Claim this name even though another session holds it now. That session loses it.'),
155
- last: z.number().int().min(1).max(HISTORY_MAX_LIMIT).optional().describe('How many recent public activities to show on arrival (default 10, up to 100).'),
86
+ last: z.number().int().min(1).max(HISTORY_MAX_LIMIT).optional().describe('How many recent public activities to show on arrival (default 10).'),
156
87
  }, (square, { as, takeover, last }, control, identity) => square.join(requireJoinName(as), { ...identity, owner: requireOwner(identity.owner), takeover, last, observe: true }, control), rendering((result) => renderJoinResult(result, undefined, 'tool'))),
157
88
  tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
158
89
  as: participant,
159
- body: z.string().optional().describe('What you say or do. Markdown works; *asterisks* mark an action. Required unless retrying with a draft token.'),
160
- draft: z.string().optional().describe('Retry a failed express kept in this process. Pass only the token; the original body and options are restored.'),
90
+ body: z.string().describe('What you say or do. Markdown works; *asterisks* mark an action.'),
161
91
  mentions: stringArray.optional().describe('Who you are speaking to. An @name in the body that matches someone here counts too.'),
162
- reach: z.enum(['bell']).optional().describe('"bell" calls everyone in the square. Cannot be used with mentions.'),
92
+ bell: z.boolean().optional().describe('Call everyone in the square. Not with mentions.'),
163
93
  reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
164
94
  noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
165
95
  force: z.boolean().optional().describe('Speak now, even if something reached you that you have not perceived yet.'),
166
96
  noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
167
- }, async (square, { body, draft, as, ...options }, control, identity) => {
168
- if (draft !== undefined)
169
- throw new SquareError('invalid_args', 'Unknown or expired express draft.');
170
- if (body === undefined)
171
- throw new SquareError('invalid_args', 'Express needs a body or a draft token.');
172
- return (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control);
173
- }, rendering((result) => renderExpression(result.observation?.location ?? '', result, undefined, 'tool'))),
97
+ }, async (square, { body, bell, as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, ...(bell ? { reach: 'bell' } : {}), observe: true }, control), rendering((result) => renderExpression(result.observation?.location ?? '', result, undefined, 'tool'))),
174
98
  tool('perceive', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you perceive no longer waits for you; history lets you look back.', {
175
99
  as: participant,
176
- id: activityId.optional().describe('Take in this activity in full, like act/12, even if you perceived it before. Do not combine with idle, from, mention, limit or takeover.'),
177
- idle: z.number().finite().min(0).optional().describe('When nothing is waiting, wait until something arrives or until it has been quiet this many milliseconds. Leave it out to return at once.'),
100
+ id: activityId.optional().describe('Open this one activity in full, like act/12. Use it alone.'),
101
+ idle: z.string().optional().describe('When nothing is waiting, wait for activity or this much quiet, like "30s". Leave it out to return at once.'),
178
102
  from: stringArray.optional().describe('Only what these participants said.'),
179
103
  mention: z.boolean().optional().describe('Only what was addressed to you, and bells.'),
180
- limit: z.number().int().min(1).optional().describe('At most this many at once (default 10, up to 100). The rest keeps waiting.'),
104
+ limit: z.number().int().min(1).max(PERCEIVE_MAX_LIMIT).optional().describe('At most this many at once (default 10). The rest keeps waiting.'),
181
105
  takeover: z.boolean().optional().describe('Take over from another active idle perceive for this participant.'),
182
- }, async (square, { as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).perceive({ ...options, observe: true }, control), rendering((result, args) => {
106
+ }, async (square, { as, idle, ...args }, control, identity) => {
107
+ const options = { ...args, ...(idle === undefined ? {} : { idle: parseDurationMs(idle, 'idle') }) };
108
+ return { ...(await (await selectOwnSeat(square, identity.owner, as)).perceive({ ...options, observe: true }, control)), options };
109
+ }, rendering((result) => {
183
110
  // The CLI always spells the default cap in its continuation, so the tool does too.
184
- const options = args;
111
+ const options = result.options;
185
112
  const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? PERCEIVE_DEFAULT_LIMIT } : options;
186
113
  return renderPerceiveOutcome(result.observation?.location ?? '', result, renderOptions, undefined, 'tool').stdout;
187
114
  })),
188
115
  tool('history', 'Look back at what was said. Reading here takes nothing in: whatever waits for perceive keeps waiting. Page with before or after.', {
189
- limit: z.number().int().min(1).optional().describe('How many per page (default 10, up to 100).'),
116
+ limit: z.number().int().min(1).max(HISTORY_MAX_LIMIT).optional().describe('How many per page (default 10).'),
190
117
  order: z.enum(['asc', 'desc']).optional().describe('"asc" for oldest first (default), "desc" for newest first.'),
191
118
  before: activityId.optional().describe('The page just before this activity, like act/12.'),
192
119
  after: activityId.optional().describe('The page just after this activity, like act/12.'),
193
120
  at: z.array(activityId).min(1).optional().describe('Center on these activity ids, like ["act/12"].'),
194
- context: z.number().int().min(0).optional().describe('Activities of context before and after each "at" activity.'),
195
- contextBefore: z.number().int().min(0).optional().describe('Activities of context before each "at" activity.'),
196
- contextAfter: z.number().int().min(0).optional().describe('Activities of context after each "at" activity.'),
121
+ context: z.number().int().min(0).max(HISTORY_MAX_LIMIT).optional().describe('Activities of context before and after each "at" activity.'),
197
122
  since: z.string().optional().describe('Only activities after this ISO timestamp or relative duration (like "10m").'),
198
- grep: z.string().optional().describe('Only activities matching this regular expression in their names, ids or words. Use grep or fixed, not both.'),
199
- fixed: z.string().optional().describe('Only activities containing this exact text.'),
123
+ search: z.string().optional().describe('Only activities containing this text in their names, ids or words.'),
124
+ regex: z.boolean().optional().describe('Treat search as a regular expression.'),
200
125
  from: stringArray.optional().describe('Only what these participants said.'),
201
126
  mention: z.string().optional().describe('Only what was addressed to this participant.'),
202
- noTruncate: z.boolean().optional().describe('Show full activity bodies even when multiple activities are returned.'),
127
+ full: z.boolean().optional().describe('Show full activity bodies even when multiple activities are returned.'),
203
128
  }, async (square, args, control, _identity, now) => {
204
129
  const query = historyToolQuery(args, now?.());
205
130
  return { ...(await square.history({ ...query, observe: true }, control)), query };
206
- }, rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: result.query, noTruncate: args.noTruncate === true, surface: 'tool' }))),
131
+ }, rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: result.query, full: args.full === true, surface: 'tool' }))),
207
132
  tool('attention', 'Turn toward someone or away from them, from now on. Listen: hear everything they say, even to others. Unlisten: stop listening without clearing an ignore; mentions and replies still reach you unless ignored. Ignore: hear nothing from them but bells. What was already said stays as it was.', {
208
133
  as: participant, action: z.enum(['listen', 'unlisten', 'ignore']).describe('"listen", "unlisten" or "ignore". Listening again ends an ignore.'), target,
209
- }, async (square, { as, action, target }, control, identity) => {
210
- const self = await selectOwnSeat(square, identity.owner, as);
211
- return self[action](target, { observe: true }, control);
212
- }, rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
134
+ }, async (square, { as, action, target }, control, identity) => (await selectOwnSeat(square, identity.owner, as))[action](target, { observe: true }, control), rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
213
135
  tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).done(undefined, { observe: true }, control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', result))),
214
136
  tool('status', 'Look around the square: who is here, the latest activity, and who you listen to.', {
215
137
  as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
@@ -231,13 +153,5 @@ export function squareToolFailure(error, signal) {
231
153
  }
232
154
  /** Readable failure text with tool-native recovery actions, never raw JSON. */
233
155
  export function squareToolFailureText(failure, context = {}) {
234
- const report = failure;
235
- const lines = renderFailureLines(report, { squarePath: context.location, name: context.participant, surface: 'tool' });
236
- if (context.draft !== undefined) {
237
- lines.push(`· draft kept in memory: ${context.draft}`);
238
- lines.push(report.code === 'behind'
239
- ? '· perceive the pending activity, then retry express with only:'
240
- : '· correct the problem above, then retry express with only:', `{ "draft": "${context.draft}" }`);
241
- }
242
- return lines.join('\n');
156
+ return renderFailureLines(failure, { squarePath: context.location, name: context.participant, surface: 'tool' }).join('\n');
243
157
  }
@@ -1,4 +1,4 @@
1
- export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue", "claude-native", "paseo", "http"];
1
+ export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue", "paseo", "http"];
2
2
  export type WakeRouteKind = typeof WAKE_ROUTE_KINDS[number];
3
3
  export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
4
4
  readonly [key: string]: JsonValue;
@@ -1,5 +1,5 @@
1
1
  import { SquareError, nameKey } from './neutral.js';
2
- export const WAKE_ROUTE_KINDS = ['opencode-server', 'codex-queue', 'claude-native', 'paseo', 'http'];
2
+ export const WAKE_ROUTE_KINDS = ['opencode-server', 'codex-queue', 'paseo', 'http'];
3
3
  export const MESSAGE_METADATA_MAX_BYTES = 16 * 1024;
4
4
  export const MESSAGE_METADATA_MAX_DEPTH = 256;
5
5
  /** Validate the bounded, lossless JSON object carried by a message. */
package/dist/time.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function parseDurationMs(value: string, label: string): number;
1
2
  export declare function formatTimestamp(at: number): string;
2
3
  export declare function formatDuration(ms: number | undefined): string;
3
4
  export declare function formatRelativeTime(at: number, now?: number): string;