@astrosheep/square 0.3.83 → 0.3.84

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 (36) hide show
  1. package/dist/claude-delivery-status.d.ts +7 -0
  2. package/dist/claude-delivery-status.js +24 -0
  3. package/dist/claude-delivery.js +9 -15
  4. package/dist/claude-mod.js +5 -0
  5. package/dist/cli/catch-presentation.d.ts +2 -6
  6. package/dist/cli/catch-presentation.js +2 -50
  7. package/dist/cli/express-presentation.d.ts +2 -2
  8. package/dist/cli/express-presentation.js +6 -29
  9. package/dist/cli/observation-commands.js +10 -142
  10. package/dist/cli/program.js +5 -28
  11. package/dist/cli/square-commands.js +14 -56
  12. package/dist/delivery-health.d.ts +2 -2
  13. package/dist/delivery-health.js +20 -2
  14. package/dist/host-ledger-file-adapter.d.ts +3 -0
  15. package/dist/host-ledger-file-adapter.js +12 -0
  16. package/dist/host-ledger.d.ts +12 -0
  17. package/dist/mcp.js +17 -6
  18. package/dist/operation-presentation.d.ts +45 -0
  19. package/dist/operation-presentation.js +370 -0
  20. package/dist/packages/agent-delivery/src/claude-native.js +3 -1
  21. package/dist/routes.js +7 -2
  22. package/dist/square-tools.d.ts +14 -2
  23. package/dist/square-tools.js +36 -17
  24. package/dist/wake-evidence.d.ts +2 -1
  25. package/dist/wake-evidence.js +2 -1
  26. package/guides/catch-cold-path-optimization.md +1 -1
  27. package/guides/claude-native.md +32 -0
  28. package/package.json +1 -1
  29. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  30. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  31. package/plugins/paseo/OVERVIEW.md +7 -4
  32. package/plugins/paseo/README.md +30 -6
  33. package/plugins/paseo/index.server.ts +10 -0
  34. package/plugins/paseo/package.json +3 -3
  35. package/plugins/paseo/paseo-plugin.json +1 -1
  36. package/plugins/paseo/server/record-delivery.ts +28 -0
@@ -0,0 +1,7 @@
1
+ import type { NativeDeliveryObservation } from './host-ledger.js';
2
+ export interface ClaudeDeliveryStatus extends NativeDeliveryObservation {
3
+ readonly provider: 'claude';
4
+ readonly sessionId: string;
5
+ }
6
+ /** This receiver-owned observation is not a model tool and grants no delivery authority. */
7
+ export declare function recordClaudeDeliveryStatus(input: ClaudeDeliveryStatus, env?: NodeJS.ProcessEnv): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ import { hostLedgerForEnv } from './registry.js';
2
+ const holdReasons = new Set(['explicit-setting', 'managed-setting', 'repo-setting', 'invalid-setting', 'bypass-default', 'mode-unknown', 'mode-mismatch', 'no-mode-asserted']);
3
+ const dropReasons = new Set(['expired', 'refused', 'dropped', 'discarded']);
4
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ /** This receiver-owned observation is not a model tool and grants no delivery authority. */
6
+ export async function recordClaudeDeliveryStatus(input, env = process.env) {
7
+ if (!input || input.provider !== 'claude' || typeof input.sessionId !== 'string' || !input.sessionId
8
+ || typeof input.messageId !== 'string' || !uuid.test(input.messageId)
9
+ || typeof input.eventId !== 'string' || !uuid.test(input.eventId)
10
+ || !Number.isFinite(input.observedAt) || input.observedAt < 0
11
+ || !['held', 'released', 'dropped'].includes(input.state)
12
+ || (input.state === 'held' ? !holdReasons.has(input.reason ?? '')
13
+ : input.state === 'dropped' ? !dropReasons.has(input.reason ?? '') : input.reason !== undefined))
14
+ return false;
15
+ const ledger = hostLedgerForEnv(env);
16
+ const rows = await ledger.listEvidence({ kind: 'wake', session: input.sessionId, includeReleased: true });
17
+ const row = rows.find((row) => row.nativeDelivery?.harness === 'claude' && row.nativeDelivery.messageId === input.messageId);
18
+ if (!row?.claimToken)
19
+ return false;
20
+ const { eventId, messageId, state, reason, observedAt } = input;
21
+ return ledger.observeNativeDelivery({ ...row, claimToken: row.claimToken }, {
22
+ eventId, messageId, state, ...(reason === undefined ? {} : { reason }), observedAt,
23
+ });
24
+ }
@@ -1,4 +1,5 @@
1
1
  import { stat } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { openSquare } from './open-square.js';
3
4
  import { closeOpenSquare } from './open-square.js';
4
5
  import { hostLedgerForEnv } from './registry.js';
@@ -7,7 +8,7 @@ import { nativePendingPreview } from './inbox.js';
7
8
  import { presentPending } from './presentation-operations.js';
8
9
  import { canonicalPath } from './canonical-path.js';
9
10
  import { parseActivityId } from './square-core.js';
10
- import { DeliveryError, connect } from './packages/agent-delivery/src/index.js';
11
+ import { writeClaudeNative } from './packages/agent-delivery/src/claude-native.js';
11
12
  const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
12
13
  export function nativeTokens(text) { return [...text.matchAll(marker)].map((match) => match[1]); }
13
14
  export function nativePayload(token, preview) { return `[square-inbox:${token}]\n${preview}\n[/square-inbox:${token}]`; }
@@ -17,18 +18,11 @@ function nativeSupported(route) { return route.address.loaded === 'true' && !!ro
17
18
  * owns the payload, the correlation and the evidence. A local write is never proof
18
19
  * of native admission.
19
20
  */
20
- async function writeClaudeWake(sessionId, endpoint, payload, timeoutMs, signal) {
21
- try {
22
- const agent = await connect({ harness: 'claude', sessionId, endpoint, timeoutMs });
23
- await agent.steer(payload, { signal });
24
- return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: 'Claude accepted the local write; native admission is unconfirmed.' };
25
- }
26
- catch (error) {
27
- if (error instanceof DeliveryError && !error.maybeDelivered) {
28
- return { outcome: 'failed', unavailable: true, retainRoute: true, signature: 'claude_endpoint_unavailable', message: error.message };
29
- }
30
- return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: error instanceof Error ? error.message : String(error) };
31
- }
21
+ async function writeClaudeWake(sessionId, endpoint, messageId, payload, timeoutMs, signal) {
22
+ const result = await writeClaudeNative({ sessionId, endpoint, messageId }, payload, { deadline: Date.now() + timeoutMs, signal });
23
+ if (result.outcome === 'unavailable')
24
+ return { outcome: 'failed', unavailable: true, retainRoute: true, signature: 'claude_endpoint_unavailable', message: result.message };
25
+ return { outcome: 'unknown', signature: 'native_admission_unconfirmed', message: result.outcome === 'written' ? 'Claude accepted the local write; native admission is unconfirmed.' : result.message };
32
26
  }
33
27
  export function createClaudeWakeTransport(hostLedger, env = process.env) {
34
28
  return {
@@ -92,13 +86,13 @@ async function dispatchClaude(request, hostLedger, timeoutMs, beforeSend, env =
92
86
  }
93
87
  if (!await (beforeSend?.() ?? Promise.resolve(true)))
94
88
  return { outcome: 'not-capable', diagnostic: 'Native activity was consumed before send.' };
95
- const native = { harness: 'claude', payload, epoch: owner.epoch ?? 0, endpoint: request.route.address.endpoint };
89
+ const native = { harness: 'claude', payload, epoch: owner.epoch ?? 0, endpoint: request.route.address.endpoint, messageId: randomUUID() };
96
90
  const row = { location: request.location, participant: request.participant, session: request.route.sessionId, activity: request.activity, kind: 'wake', outcome: 'dispatching', routeKind: 'claude-native', attemptN: request.attemptN, claimToken: request.claimToken, nativeDelivery: native };
97
91
  if (!await currentDelivery(row, hostLedger) || !await hostLedger.prepareNativeWake(row))
98
92
  return { outcome: 'not-capable', diagnostic: 'Native delivery is no longer current.' };
99
93
  if (!await (beforeSend?.() ?? Promise.resolve(true)) || !await currentDelivery(row, hostLedger))
100
94
  return { outcome: 'not-capable', diagnostic: 'Catch, cancellation or consumption won before native write.' };
101
- return writeClaudeWake(row.session, native.endpoint, payload, timeoutMs, signal);
95
+ return writeClaudeWake(row.session, native.endpoint, native.messageId, payload, timeoutMs, signal);
102
96
  }, signal);
103
97
  }
104
98
  /** Retired tokens veto a frame; only live correlations and bound memberships confer authority. */
@@ -8,6 +8,7 @@ import { observeClaudeDelivery } from './claude-delivery.js';
8
8
  import { processActNotificationsOnce } from './notifications.js';
9
9
  import { sweepPending } from './delivery-operations.js';
10
10
  import { beginClaudeMcpCall, endClaudeMcpCall } from './claude-mcp-context.js';
11
+ import { recordClaudeDeliveryStatus } from './claude-delivery-status.js';
11
12
  /** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
12
13
  export async function runClaudeMod(inputText, env = process.env) {
13
14
  if (inputText.length > 512_000)
@@ -15,6 +16,10 @@ export async function runClaudeMod(inputText, env = process.env) {
15
16
  const input = JSON.parse(inputText);
16
17
  if (!input || typeof input.sessionId !== 'string' || !input.sessionId || typeof input.cwd !== 'string')
17
18
  throw new Error('Claude bridge coordinate missing.');
19
+ if (input.operation === 'status') {
20
+ const recorded = input.status?.sessionId === input.sessionId && await recordClaudeDeliveryStatus(input.status, env);
21
+ return `${JSON.stringify({ recorded })}\n`;
22
+ }
18
23
  if (input.operation === 'tool-begin') {
19
24
  return `${JSON.stringify(await beginClaudeMcpCall({ toolUseId: input.toolUseId, sessionId: input.sessionId, cwd: input.cwd, agentId: input.agentId }, env))}\n`;
20
25
  }
@@ -1,7 +1,3 @@
1
- import type { CatchResult, CatchOptions, OperationProgress } from '../square-facade.js';
1
+ import type { OperationProgress } from '../square-facade.js';
2
+ export { renderCatchOutcome } from '../operation-presentation.js';
2
3
  export declare function renderCatchProgress(squarePath: string, progress: OperationProgress): string | undefined;
3
- /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
4
- export declare function renderCatchOutcome(squarePath: string, data: CatchResult, options?: CatchOptions): {
5
- stdout: string;
6
- exitCode: number;
7
- };
@@ -1,6 +1,5 @@
1
- import { parseActivityId } from '../square-api.js';
2
- import { participantCommandPrefix, renderAmbientEvent, renderWatchAlreadyActive, renderWatchForceTakeover, renderWatchReplaceMissing, renderWatchReplaced, renderWatchStatus, withPathOutput, } from '../presentation.js';
3
- import { style } from '../tty-style.js';
1
+ import { renderWatchForceTakeover, renderWatchReplaceMissing } from '../presentation.js';
2
+ export { renderCatchOutcome } from '../operation-presentation.js';
4
3
  export function renderCatchProgress(squarePath, progress) {
5
4
  if (progress.kind !== 'catch-started' || !progress.replacementRequested)
6
5
  return undefined;
@@ -8,50 +7,3 @@ export function renderCatchProgress(squarePath, progress) {
8
7
  return (progress.replaced
9
8
  ? renderWatchForceTakeover(options) : renderWatchReplaceMissing(options)) + '\n';
10
9
  }
11
- function continuationCommand(squarePath, name, options) {
12
- const args = ['--now'];
13
- if (options.from?.length)
14
- args.push('--from', options.from.join(','));
15
- if (options.mention)
16
- args.push('--mention');
17
- if (options.limit !== undefined)
18
- args.push('--limit', String(options.limit));
19
- return `${participantCommandPrefix(squarePath, name)} catch ${args.join(' ')}`;
20
- }
21
- /** Render settled catch facts; no selection, terminal decisions, receipts or wait machinery. */
22
- export function renderCatchOutcome(squarePath, data, options = {}) {
23
- const projection = data.observation;
24
- const location = projection.location ?? squarePath;
25
- const name = projection.participant;
26
- const stop = projection.stop ?? data.status;
27
- const header = { participantCount: projection.standingCount, held: projection.held };
28
- const statusOptions = {
29
- squarePath: location, name, presence: projection.presence,
30
- showCatchHint: !projection.automaticDeliveryAvailable,
31
- ownActivityCount: projection.ownActivityCount, hardCap: projection.hardCap,
32
- };
33
- if (stop === 'already-active' || stop === 'replaced') {
34
- return {
35
- stdout: withPathOutput(location, stop === 'already-active'
36
- ? renderWatchAlreadyActive(statusOptions) : renderWatchReplaced(statusOptions), header),
37
- exitCode: stop === 'already-active' ? 1 : 0,
38
- };
39
- }
40
- const rendered = data.activities.map((activity) => renderAmbientEvent(activity, name, {
41
- now: projection.now, actNumber: projection.sayNumbers[parseActivityId(activity.id)],
42
- perception: activity.perception, mention: options.mention ? name : undefined,
43
- })).filter(Boolean).join('\n\n');
44
- const remaining = data.remaining === 0 ? ''
45
- : `○ ${data.remaining} matching ${data.remaining === 1 ? 'activity remains' : 'activities remain'}\n${continuationCommand(location, name, options)}`;
46
- const terminal = stop === undefined ? '' : renderWatchStatus({
47
- ...statusOptions, status: stop,
48
- ...(stop === 'stale' && options.idle !== undefined ? { idleMs: options.idle } : {}),
49
- });
50
- const fallback = stop === undefined && !projection.automaticDeliveryAvailable && data.remaining === 0
51
- ? `${participantCommandPrefix(location, name)} catch --idle 30m\n stay available for new activity` : '';
52
- const hint = data.hint === undefined ? '' : style('dim', `· ${data.hint}`);
53
- return {
54
- stdout: withPathOutput(location, [terminal, rendered, hint, remaining, fallback].filter(Boolean).join('\n\n'), header),
55
- exitCode: stop === 'capped' ? 1 : 0,
56
- };
57
- }
@@ -1,11 +1,11 @@
1
1
  import { type Square } from '../square-api.js';
2
- import type { OperationControl, ExpressResult, ExpressOptions } from '../square-facade.js';
2
+ import type { OperationControl, ExpressOptions } from '../square-facade.js';
3
+ export { renderExpression } from '../operation-presentation.js';
3
4
  export interface ActivityOptions extends Omit<ExpressOptions, 'reply'> {
4
5
  readonly reply?: string;
5
6
  }
6
7
  export declare function saveActivityDraft(squarePath: string, name: string, body: string, now?: number): string;
7
8
  /** Retry choices are transport syntax, never a second admission decision. */
8
9
  export declare function activityRetryCommand(squarePath: string, name: string, opts: ActivityOptions, force?: boolean): string;
9
- export declare function renderExpression(squarePath: string, data: ExpressResult): string;
10
10
  /** Body adaptation, terminal IO and drafts only; Participant owns every attempt and wait. */
11
11
  export declare function executeExpression(open: (control?: OperationControl) => Promise<Square>, squarePath: string, requestedName: string | undefined, rawInput: string, opts: ActivityOptions): Promise<void>;
@@ -3,9 +3,10 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { isSquareError } from '../model.js';
5
5
  import { formatActivityId, parseActivityId } from '../square-api.js';
6
- import { formatRefusal, refusalText, renderActivityBlocked, renderActivityLimit, renderActivityUncertain, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, participantCommandPrefix, commandPrefix, quoteShell, takeoverRecoveryLines, withPathOutput, } from '../presentation.js';
6
+ import { formatRefusal, refusalText, renderActivityBlocked, renderActivityLimit, renderActivityUncertain, renderExpressNoWait, renderExpressWaiting, joinRecoveryCommand, participantsRecoveryCommand, participantCommandPrefix, commandPrefix, quoteShell, takeoverRecoveryLines, withPathOutput, } from '../presentation.js';
7
+ import { renderExpression } from '../operation-presentation.js';
7
8
  import { formatDuration } from '../time.js';
8
- import { style } from '../tty-style.js';
9
+ export { renderExpression } from '../operation-presentation.js';
9
10
  export function saveActivityDraft(squarePath, name, body, now = Date.now()) {
10
11
  const draftDir = path.join(path.dirname(squarePath), 'drafts');
11
12
  fs.mkdirSync(draftDir, { recursive: true });
@@ -38,32 +39,6 @@ export function activityRetryCommand(squarePath, name, opts, force = opts.force
38
39
  + (opts.noMention === true ? ' --no-mention' : '');
39
40
  return `${participantCommandPrefix(squarePath, name)} express${force ? ' --force' : ''}${opts.noWait ? ' --no-wait' : ''}${reach}${replyArgument(opts.reply)} -`;
40
41
  }
41
- function isGestureOnly(body) {
42
- return /\*[^*]*\*/.test(body) && body.replace(/\*[^*]*\*/g, '').trim() === '';
43
- }
44
- export function renderExpression(squarePath, data) {
45
- const projection = data.observation;
46
- const name = projection.participant;
47
- const pendingSpeaker = projection.pendingPublic[0]?.actor;
48
- const pendingLead = pendingSpeaker === undefined ? ''
49
- : `▲ while you spoke, ${participantIdentity(pendingSpeaker)} said something behind you\n\n`;
50
- const pending = projection.pendingPublic.length === 0 ? ''
51
- : `\n\n${pendingLead}${renderPendingFeed(projection.pendingPublic, projection.sayNumbers, projection.perceptions, name)}`;
52
- const ownCount = projection.ownActivityCount;
53
- const capCount = projection.hardCap === null ? '' : ` · ${ownCount}/${projection.hardCap}`;
54
- const activity = data.activity;
55
- const confirmation = activity.reach === 'bell'
56
- ? `● you ring the bell — everyone turns · ${activity.id}${style('dim', capCount)}`
57
- : `● ${activity.mentions.length > 0
58
- ? `you walk over to ${activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
59
- : isGestureOnly(activity.body ?? '') ? 'your gesture lands' : 'your words land'} — ${activity.id}${style('dim', capCount)}`;
60
- const replyNotice = data.replyAuthorNotHere === undefined ? ''
61
- : `\n○ ${participantIdentity(data.replyAuthorNotHere)} isn't in the square right now — the reply landed without reaching them`;
62
- const unmatchedNotice = data.unmatchedMentions?.length
63
- ? `\n · ${data.unmatchedMentions.map((token) => `@${token} isn't standing here`).join('; ')} — ${activity.mentions.length === 0 && activity.reply === undefined ? 'activity landed bare' : 'left as literal text'}` : '';
64
- return withPathOutput(projection.location ?? squarePath, (projection.reentry === undefined ? '' : '● you stepped back into the square\n')
65
- + confirmation + (data.hint ? `\n${style('dim', `· ${data.hint}`)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount, held: projection.held });
66
- }
67
42
  /** Body adaptation, terminal IO and drafts only; Participant owns every attempt and wait. */
68
43
  export async function executeExpression(open, squarePath, requestedName, rawInput, opts) {
69
44
  let square;
@@ -92,7 +67,9 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
92
67
  });
93
68
  landedId = outcome.activity.id;
94
69
  name = outcome.observation.participant;
95
- process.stdout.write(renderExpression(squarePath, outcome));
70
+ const rendered = renderExpression(squarePath, outcome);
71
+ if (rendered !== undefined)
72
+ process.stdout.write(rendered);
96
73
  }
97
74
  catch (error) {
98
75
  const detail = error?.operation;
@@ -2,16 +2,14 @@ import path from 'node:path';
2
2
  import { once } from 'node:events';
3
3
  import { runClaudeMod } from '../claude-mod.js';
4
4
  import { runCodexHookAsync } from '../codex-hook.js';
5
- import { isSquareError, sameName } from '../model.js';
6
- import { commandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEventFacts, renderPresenceAnchor, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
5
+ import { isSquareError } from '../model.js';
6
+ import { commandPrefix, participantIdentity, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
7
+ import { PARTICIPANTS_DEFAULT_LIMIT, PARTICIPANTS_MAX_LIMIT, renderHistoryResult, renderParticipantsResult, renderStatusResult, } from '../operation-presentation.js';
7
8
  import { style } from '../tty-style.js';
8
9
  import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
9
10
  import { discover, inbox, 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-api.js';
10
11
  import { renderCatchOutcome, renderCatchProgress } from './catch-presentation.js';
11
12
  import { withSquare, squareInput, now, fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireSquarePath, requireValue, usage, } from './context.js';
12
- const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
13
- const PARTICIPANTS_DEFAULT_LIMIT = 20;
14
- const PARTICIPANTS_MAX_LIMIT = 100;
15
13
  const INBOX_DEFAULT_LIMIT = 20;
16
14
  const INBOX_MAX_LIMIT = 100;
17
15
  const INBOX_DISPLAY_CHARS = 160;
@@ -414,34 +412,6 @@ function jsonLine(sayNumbers, item) {
414
412
  reply: act.kind === 'say' && act.reply !== undefined ? formatActivityId(act.reply) : null,
415
413
  });
416
414
  }
417
- function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
418
- const shown = visible;
419
- const preview = noTruncate || shown.length <= 1 ? undefined : 200;
420
- const chunks = [];
421
- // Presence anchors collect into one closing line instead of interrupting the page.
422
- const anchored = [];
423
- for (const activity of shown) {
424
- const options = {
425
- preview,
426
- actNumber: activity.kind === 'say' ? projection.sayNumbers[activity.index] : undefined,
427
- };
428
- const rendered = renderEventCli(activity, options);
429
- if (rendered !== '')
430
- chunks.push(rendered);
431
- for (const participant of projection.presenceAnchors[activity.index] ?? []) {
432
- if (!anchored.some((existing) => sameName(existing, participant)))
433
- anchored.push(participant);
434
- }
435
- }
436
- if (chunks.length === 0)
437
- return 'latest\n ○ no public activity in this view';
438
- if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview)) {
439
- chunks.push(`${commandPrefix(squarePath)} history --no-truncate`);
440
- }
441
- if (anchored.length > 0)
442
- chunks.push(renderPresenceAnchor(anchored));
443
- return chunks.join('\n\n');
444
- }
445
415
  export const historyCommand = {
446
416
  parse(argv, context) { return parseHistory(argv, context); },
447
417
  async execute(options, context) {
@@ -463,13 +433,11 @@ export const historyCommand = {
463
433
  if (options.format !== undefined && options.format.length > 0) {
464
434
  return events.map((item) => renderFields(projection.sayNumbers, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
465
435
  }
466
- const pattern = options.query.grep ?? options.query.fixed;
467
- const output = pattern === undefined || pattern === ''
468
- ? renderHistoryProjection(projection, projection.visibleActivities, options.noTruncate, squarePath)
469
- : renderGrepActivitiesView(projection.visibleActivities, projection.totalMatches, options.noTruncate, squarePath, pattern, options.query.fixed !== undefined, () => 'full');
470
- const next = projection.continuation;
471
- const continuation = next === undefined ? '' : `\n\n${historyContinuationCommand(options, squarePath, [`--${next.direction}`, next.cursor, '--limit', String(next.limit)])}`;
472
- return withPathOutput(squarePath, output + continuation, { participantCount: projection.standingCount });
436
+ return renderHistoryResult(squarePath, outcome, {
437
+ query: options.query,
438
+ noTruncate: options.noTruncate,
439
+ continuationArgs: options.continuationArgs,
440
+ });
473
441
  },
474
442
  present: (result) => process.stdout.write(result),
475
443
  };
@@ -491,26 +459,7 @@ export const participantsCommand = {
491
459
  async execute(intent, context) {
492
460
  const squarePath = requireSquarePath(context);
493
461
  const data = await withSquare(context, (square) => square.participants({ observe: true }));
494
- const projection = data.observation;
495
- const now = projection.now;
496
- const participants = data.participants;
497
- const lines = participants.slice(0, intent.limit).map((participant) => {
498
- const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
499
- const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
500
- const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
501
- return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
502
- });
503
- const tail = participants.length <= intent.limit
504
- ? []
505
- : [
506
- style('dim', ` ○ ${lines.length} of ${participants.length} participants shown`),
507
- ...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
508
- `${participantsLimitCommand(squarePath, participants.length)}`,
509
- ] : []),
510
- ];
511
- return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
512
- participantCount: projection.standingCount,
513
- });
462
+ return renderParticipantsResult(squarePath, data, intent.limit);
514
463
  },
515
464
  present: (result) => process.stdout.write(result),
516
465
  };
@@ -520,88 +469,7 @@ export const statusCommand = {
520
469
  async execute(_intent, context) {
521
470
  const squarePath = requireSquarePath(context);
522
471
  const result = await withSquare(context, (square) => square.status({ viewer: context.name, observe: true }));
523
- const presentation = result.observation;
524
- const participantsByName = new Map(result.participants.map((participant) => [participant.name, participant]));
525
- const orderedHere = presentation.orderedHereNames.map((name) => participantsByName.get(name));
526
- const lingering = presentation.lingeringCount;
527
- const people = [];
528
- if (result.activeCount === 0) {
529
- people.push(' ○ nobody in the square');
530
- }
531
- else {
532
- if (orderedHere.length === 0)
533
- people.push(' ○ nobody here right now');
534
- people.push(...orderedHere.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
535
- const glyph = participant.presence === 'watching'
536
- ? '◎'
537
- : participant.activityCount > 0 ? '●' : '○';
538
- const summary = participant.presence === 'watching'
539
- ? 'is nearby · catching'
540
- : participant.activityCount > 0
541
- ? `is nearby · ${formatRelativeTime(participant.lastActiveAt ?? result.now, result.now)}`
542
- : `is nearby · quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
543
- const showAttention = presentation.attentionNames.includes(participant.name);
544
- const attention = !showAttention
545
- ? ''
546
- : participant.pendingMentionCount > 0
547
- ? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
548
- : participant.unreadActivityCount > 0
549
- ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
550
- : 'caught up';
551
- return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
552
- }));
553
- const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
554
- if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
555
- people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
556
- }
557
- if (lingering > 0)
558
- people.push(` ○ … ${lingering} lingering`);
559
- if (unshown > 0) {
560
- people.push(`${presentation.knownParticipantCount <= PARTICIPANTS_MAX_LIMIT
561
- ? participantsLimitCommand(squarePath, presentation.knownParticipantCount)
562
- : `${commandPrefix(squarePath)} participants`}`);
563
- }
564
- }
565
- const cap = result.hardCap === null ? 'unlimited' : `${result.hardCap} each`;
566
- const holdTime = result.hold?.at === undefined ? 'just now' : formatRelativeTime(result.hold.at, result.now);
567
- const hold = result.hold !== null
568
- ? `· ${result.hold.by === undefined ? 'someone' : participantIdentity(result.hold.by)} raised a hand${result.hold.reason ? ` — ${result.hold.reason}` : ''}${style('dim', ` · ${holdTime}`)}`
569
- : undefined;
570
- const visible = result.latestActivity === undefined
571
- ? { text: '', bodyClipped: false }
572
- : renderAmbientEventFacts(result.latestActivity, presentation.viewer, {
573
- now: result.now,
574
- preview: 200,
575
- actNumber: presentation.latestActNumber,
576
- perception: presentation.latestPerception,
577
- });
578
- const latest = visible.text === ''
579
- ? [result.latestActivity === undefined
580
- ? ' ○ no public activity yet'
581
- : ' · the latest words were meant for other ears']
582
- : [` ${visible.text.replace(/\n/g, '\n ')}`];
583
- // Presence events after the latest public act: otherwise the roster can say someone is
584
- // here while the latest line shows them leaving, with nothing to reconcile the two.
585
- for (const { activity, rejoined } of presentation.joins) {
586
- latest.push(style('dim', ` → ${participantIdentity(activity.actor)} ${rejoined ? 'stepped back in' : 'stepped in'} · ${formatRelativeTime(activity.at, result.now)}`));
587
- }
588
- if (presentation.joinCount > 3)
589
- latest.push(style('dim', ` → … ${presentation.joinCount - 3} more stepped in`));
590
- if (visible.bodyClipped && result.latestActivity !== undefined) {
591
- latest.push(`${commandPrefix(squarePath)} history --at ${result.latestActivity.id} -C 2 --no-truncate`);
592
- }
593
- const counts = [
594
- `${orderedHere.length} here`,
595
- ...(lingering > 0 ? [`${lingering} lingering`] : []),
596
- `${result.doneCount} done`,
597
- `cap ${cap}`,
598
- `throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
599
- ].join(' · ');
600
- const output = [
601
- style('dim', counts),
602
- ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
603
- ].join('\n');
604
- return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: presentation.held });
472
+ return renderStatusResult(squarePath, result);
605
473
  },
606
474
  present: (result) => process.stdout.write(result),
607
475
  };
@@ -1,6 +1,7 @@
1
1
  import { helpRequest } from '../help.js';
2
2
  import { isSquareError } from '../model.js';
3
- import { commandPrefix, formatRefusal, refusalText, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, takeoverRecoveryLines, } from '../presentation.js';
3
+ import { formatRefusal } from '../presentation.js';
4
+ import { renderFailureLines } from '../operation-presentation.js';
4
5
  import { withSquare, defaultContext, parseGlobalArgs } from './context.js';
5
6
  import { executeRegisteredCommand, findCommand } from './registry.js';
6
7
  async function handleSquareError(error, context) {
@@ -11,35 +12,11 @@ async function handleSquareError(error, context) {
11
12
  const recovery = error.facts?.reason === 'migration_required' || context === undefined || squarePath === undefined ? undefined
12
13
  : await withSquare({ ...context, name: detail?.participant ?? context.name }, (square) => square.recovery(detail?.participant ?? context.name)).catch(() => undefined);
13
14
  const name = detail?.participant ?? recovery?.participant ?? context?.name;
14
- const existing = [refusalText(error)];
15
- if (error.facts?.reason === 'migration_required') {
16
- if (error.facts.ledgerRoot !== undefined)
17
- existing.push('square ledger migrate', 'square ledger verify');
18
- else if (squarePath !== undefined)
19
- existing.push(`${commandPrefix(squarePath)} migrate`);
20
- }
21
- else if (squarePath !== undefined && name !== undefined && error.code === 'already_joined') {
22
- existing.splice(0, 1, `✕ ${participantIdentity(name)} shoos you out of the square`, ` · ${participantIdentity(name)} already stands here — another session holds the name`);
23
- existing.push(...takeoverRecoveryLines(squarePath, name));
24
- }
25
- else if (squarePath !== undefined && error.facts?.reason === 'never_joined') {
26
- existing.push(detail?.subject !== 'filter' && name !== undefined
27
- ? joinRecoveryCommand(squarePath, name) : participantsRecoveryCommand(squarePath));
28
- }
29
- else if (squarePath !== undefined && error.facts?.reason === 'not_standing') {
30
- existing.push(participantsRecoveryCommand(squarePath));
31
- }
32
- else if (squarePath !== undefined && error.facts?.reason === 'not_addressed' && typeof error.facts.activityId === 'string') {
33
- existing.push(error.facts.activityExists === true
34
- ? `${commandPrefix(squarePath)} history --at ${error.facts.activityId} -C 2`
35
- : `${commandPrefix(squarePath)} history --limit 10`);
36
- }
37
- if (error.facts?.reason === 'invalid_done_body')
38
- existing.push('square done --help');
15
+ const lines = renderFailureLines(error, { squarePath, name });
39
16
  if (squarePath === undefined)
40
- process.stderr.write(`${existing.join('\n')}\n`);
17
+ process.stderr.write(`${lines.join('\n')}\n`);
41
18
  else
42
- process.stderr.write(formatRefusal(squarePath, existing, {
19
+ process.stderr.write(formatRefusal(squarePath, lines, {
43
20
  participantCount: recovery?.standingCount, held: recovery?.held,
44
21
  }));
45
22
  process.exit(error.code === 'not_found' ? 1 : 2);
@@ -1,7 +1,7 @@
1
1
  import { formatHardCap } from '../model.js';
2
- import { commandPrefix, participantCommandPrefix, participantIdentity, renderRoomChangeText, renderAmbientEvent, withPathOutput, } from '../presentation.js';
3
- import { Square, formatActivityId } from '../square-api.js';
4
- import { style } from '../tty-style.js';
2
+ import { commandPrefix, participantCommandPrefix, withPathOutput, } from '../presentation.js';
3
+ import { renderJoinResult, renderLifecycleResult, renderListenerChange, renderListeningResult, } from '../operation-presentation.js';
4
+ import { Square } from '../square-api.js';
5
5
  import { executeExpression } from './express-presentation.js';
6
6
  import { squareInput, withSquare, fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
7
7
  const JOIN_DEFAULT_LAST = 10;
@@ -76,33 +76,7 @@ export const joinCommand = {
76
76
  },
77
77
  async execute(intent, context) {
78
78
  const data = await withSquare(context, (square) => square.join(context.name, { takeover: intent.kick, last: intent.lastN, observe: true }));
79
- const after = data.observation;
80
- const entry = after.entry;
81
- const name = data.participant.name;
82
- const activities = (entry?.recentActivities ?? []).map((event) => renderAmbientEvent(event, name, {
83
- now: after.now, preview: 200,
84
- actNumber: event.kind === 'say' ? entry.sayNumbers[event.index] : undefined,
85
- perception: entry.perceptions[event.index],
86
- })).filter(Boolean).join('\n\n');
87
- const fallback = after.automaticDeliveryAvailable ? [] : [
88
- '', `${participantCommandPrefix(after.location, name)} catch --idle 30m`,
89
- ' the square has no way to call you — keep this catch open and stay within earshot',
90
- ];
91
- const entryLine = data.kind === 'joined' ? '● you stepped into the square'
92
- : data.kind === 'reconnected' ? '● you are already in the square'
93
- : `✓ you banished the original ${participantIdentity(name)} — the name is yours`;
94
- const isRejoin = data.kind !== 'joined';
95
- const entryScene = isRejoin || entry === undefined ? '' : entry.joinedBefore
96
- ? '*the flagstones are where you left them. so is your shadow.*' : entry.scene;
97
- return withPathOutput(after.location, [
98
- entryLine, '',
99
- "· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
100
- ...(entryScene === '' ? [] : ['', entryScene]),
101
- ...(isRejoin || !entry?.joinContext ? [] : ['', style('dim', 'context'), entry.joinContext]),
102
- ...(after.enrichmentUnavailable ? [' · joined; recent activity and context are unavailable right now'] : []),
103
- ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
104
- ...fallback,
105
- ].join('\n'), { participantCount: after.standingCount, held: after.held });
79
+ return renderJoinResult(data);
106
80
  },
107
81
  present: (result) => process.stdout.write(result),
108
82
  };
@@ -166,20 +140,9 @@ function listenerCommand(verb) {
166
140
  return {
167
141
  parse(argv, context) { return parseListener(argv, context, true); },
168
142
  async execute(intent, context) {
143
+ const squarePath = requireSquarePath(context);
169
144
  const data = await withSquare(context, async (square) => (await square.participant(context.name))[verb](intent.target, { observe: true }));
170
- const projection = data.observation;
171
- const actor = participantIdentity(projection.participant);
172
- const target = participantIdentity(projection.target);
173
- const action = data.activity !== null
174
- ? verb === 'listen' ? `${actor} turns an ear toward ${target}`
175
- : verb === 'ignore' ? `${actor} turns away from ${target}`
176
- : `${actor} stops listening to ${target}`
177
- : verb === 'listen' ? `${actor} already turns an ear toward ${target}`
178
- : verb === 'ignore' ? `${actor} is not turned toward ${target}`
179
- : `${actor} is not listening to ${target}`;
180
- return withPathOutput(projection.location, `${data.activity === null ? '○' : '·'} ${action}`, {
181
- participantCount: projection.standingCount, held: projection.held,
182
- });
145
+ return renderListenerChange(squarePath, verb, data);
183
146
  },
184
147
  present: (result) => process.stdout.write(result),
185
148
  };
@@ -190,12 +153,9 @@ export const unlistenCommand = listenerCommand('unlisten');
190
153
  export const listeningCommand = {
191
154
  parse(argv, context) { return parseListener(argv, context, false); },
192
155
  async execute(_intent, context) {
156
+ const squarePath = requireSquarePath(context);
193
157
  const data = await withSquare(context, async (square) => (await square.participant(context.name)).listening({ observe: true }));
194
- const projection = data.observation;
195
- const body = data.listening.length === 0
196
- ? `○ ${participantIdentity(projection.participant)} is not turned toward anyone`
197
- : ['listening', ...data.listening.map((target) => ` · ${participantIdentity(target)}`)].join('\n');
198
- return withPathOutput(projection.location, body, { participantCount: projection.standingCount, held: projection.held });
158
+ return renderListeningResult(squarePath, data);
199
159
  },
200
160
  present: (result) => process.stdout.write(result),
201
161
  };
@@ -208,11 +168,9 @@ export const doneCommand = {
208
168
  async execute(intent, context) {
209
169
  const body = intent.body === '-' || ((intent.body === undefined || intent.body === '') && !process.stdin.isTTY)
210
170
  ? await readStdin() : intent.body;
171
+ const squarePath = requireSquarePath(context);
211
172
  const data = await withSquare(context, async (square) => (await square.participant(context.name)).done(body, { observe: true }));
212
- const projection = data.observation;
213
- return withPathOutput(projection.location, `○ you stepped out — your footsteps fade · ${data.activity.id}`, {
214
- participantCount: projection.standingCount, held: projection.held,
215
- });
173
+ return renderLifecycleResult(squarePath, 'done', data);
216
174
  },
217
175
  present: (result) => process.stdout.write(result),
218
176
  };
@@ -224,9 +182,9 @@ export const holdCommand = {
224
182
  },
225
183
  async execute(intent, context) {
226
184
  const body = (await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim();
185
+ const squarePath = requireSquarePath(context);
227
186
  const result = await withSquare(context, async (square) => (await square.participant(context.name)).hold(body, { observe: true }));
228
- const projection = result.observation;
229
- return withPathOutput(projection.location, `· ${renderRoomChangeText(projection.activity)} · ${formatActivityId(projection.activity.index)} · just now`, { participantCount: projection.standingCount, held: projection.held });
187
+ return renderLifecycleResult(squarePath, 'hold', result);
230
188
  },
231
189
  present: (result) => process.stdout.write(result),
232
190
  };
@@ -237,9 +195,9 @@ export const resumeCommand = {
237
195
  return {};
238
196
  },
239
197
  async execute(_intent, context) {
198
+ const squarePath = requireSquarePath(context);
240
199
  const result = await withSquare(context, async (square) => (await square.participant(context.name)).resume({ observe: true }));
241
- const projection = result.observation;
242
- return withPathOutput(projection.location, `✓ ${renderRoomChangeText(projection.activity)} · ${formatActivityId(projection.activity.index)} · just now`, { participantCount: projection.standingCount, held: projection.held });
200
+ return renderLifecycleResult(squarePath, 'resume', result);
243
201
  },
244
202
  present: (result) => process.stdout.write(result),
245
203
  };
@@ -1,5 +1,5 @@
1
1
  import { type DirectedNotificationRoute } from './delivery.js';
2
- import type { WakeAttempt, WakeReleaseDiagnostic } from './wake-evidence.js';
2
+ import { type WakeAttempt, type WakeReleaseDiagnostic } from './wake-evidence.js';
3
3
  export type DeliveryHealthKind = 'awaiting' | 'wake-accepted' | 'wake-unknown' | 'presented-not-delivered' | 'unreachable';
4
4
  export interface DeliveryHealthItem {
5
5
  squarePath: string;
@@ -18,5 +18,5 @@ export declare function classifyDeliveryHealth(squarePath: string, opts: {
18
18
  now?: number;
19
19
  env?: NodeJS.ProcessEnv;
20
20
  }): Promise<DeliveryHealthItem[]>;
21
- export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[], releases?: readonly WakeReleaseDiagnostic[], now?: number): string[];
21
+ export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[], releases?: readonly WakeReleaseDiagnostic[], now?: number, nativeAttempts?: readonly WakeAttempt[]): string[];
22
22
  export declare function doctorDeliveryHealth(squarePath: string, graceMs: number, now?: number, env?: NodeJS.ProcessEnv): Promise<string[]>;