@astrosheep/square 0.3.58 → 0.3.61

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.
@@ -1,15 +1,16 @@
1
1
  import { cmdActivity } from '../activity.js';
2
2
  import { formatActivityId, formatHardCap, parseActivityId, } from '../model.js';
3
- import { participantCommandPrefix, participantIdentity, quoteShell, renderEventCli, renderAmbientEvent, withPathOutput, } from '../presentation.js';
3
+ import { formatRefusal, participantCommandPrefix, participantIdentity, quoteShell, renderRoomChangeText, renderAmbientEvent, withPathOutput, } from '../presentation.js';
4
4
  import { claimSessionParticipant, hasAutomaticDeliveryIdentity, localSessionIdentities, lookupParticipant, readParticipantOwner, recordSessionDone, } from '../registry.js';
5
5
  import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
6
6
  import { sessionIdsFromEnvironment } from '../square-projections.js';
7
- import { nowMs } from '../runtime.js';
7
+ import { actId, nowMs } from '../runtime.js';
8
8
  import { createSquare, openSquare } from '../square-file-adapter.js';
9
9
  import { closeOpenSquare } from '../open-square.js';
10
10
  import { openParticipant, Square } from '../square-wiring.js';
11
11
  import { entryPresentation, eventPresentation } from '../views.js';
12
12
  import { createDefaultWakeTransport } from '../notifications.js';
13
+ import { style } from '../tty-style.js';
13
14
  import { fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
14
15
  const JOIN_DEFAULT_LAST = 10;
15
16
  const JOIN_MAX_LAST = 100;
@@ -57,7 +58,7 @@ export const buildCommand = {
57
58
  await createSquare(squarePath, intent.options, snippet);
58
59
  const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
59
60
  const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
60
- return withPathOutput(squarePath, ['✓ built', ` · cap ${cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
61
+ return withPathOutput(squarePath, ['✓ the square is open', ` · cap ${cap}`, ...throttle, ' · nobody here yet — the first join steps in'].join('\n'), { participantCount: 0 });
61
62
  },
62
63
  present: (result) => process.stdout.write(result),
63
64
  };
@@ -95,12 +96,13 @@ export const joinCommand = {
95
96
  const reconnect = before.joined && identity !== undefined && owner?.sessionId === identity.sessionId;
96
97
  const isRejoin = before.joined;
97
98
  if (isRejoin && !intent.kick && !reconnect) {
98
- fail([
99
+ process.stderr.write(formatRefusal(squarePath, [
99
100
  `✕ ${participantIdentity(intent.name)} shoos you out of the square`,
100
101
  ` · a same-named participant stands here — the name is taken`,
101
- ` · --kick banishes her and the name becomes yours`,
102
- `» ${participantCommandPrefix(squarePath, intent.name)} join --kick`,
103
- ].join('\n'));
102
+ ` · --kick banishes the one standing there — the name becomes yours`,
103
+ `${participantCommandPrefix(squarePath, intent.name)} join --kick`,
104
+ ], { participantCount: before.participantCount }));
105
+ process.exit(2);
104
106
  }
105
107
  const takeoverNeeded = isRejoin && intent.kick;
106
108
  const oldBindings = takeoverNeeded ? await lookupParticipant(squarePath, intent.name) : [];
@@ -134,20 +136,20 @@ export const joinCommand = {
134
136
  const contextText = after.joinContext;
135
137
  const fallback = hasAutomaticDeliveryIdentity()
136
138
  ? []
137
- : ['', `» ${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
139
+ : ['', `${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' the square has no way to call you — keep this catch open and stay within earshot'];
138
140
  const scene = after.scene;
139
141
  const entryLine = !isRejoin
140
- ? '● You stepped into the square'
142
+ ? '● you stepped into the square'
141
143
  : reconnect && !intent.kick
142
144
  ? '● you are already in the square'
143
145
  : `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
144
146
  const output = [
145
147
  entryLine,
146
148
  '',
147
- "Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.",
149
+ "· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
148
150
  ...(reconnect || scene === '' ? [] : ['', scene]),
149
- ...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
150
- ...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
151
+ ...(isRejoin || contextText === '' ? [] : ['', style('dim', 'context'), contextText]),
152
+ ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
151
153
  ...fallback,
152
154
  ].join('\n');
153
155
  return withPathOutput(squarePath, output, { participantCount: after.participantCount });
@@ -173,7 +175,7 @@ function parseActivity(argv, context) {
173
175
  else if (argument === '--no-wait')
174
176
  noWait = true;
175
177
  else if (argument === '--beside')
176
- fail('✕ express does not know --beside\n» square express --help');
178
+ fail('✕ express does not know --beside\nsquare express --help');
177
179
  else if (argument === '--bell')
178
180
  bell = true;
179
181
  else if (argument === '--no-mention')
@@ -194,11 +196,11 @@ function parseActivity(argv, context) {
194
196
  }
195
197
  const reach = bell ? 'bell' : undefined;
196
198
  if (bell && (noMention || mentions.length > 0))
197
- fail('✕ --bell cannot be combined with --mention or --no-mention\n» square express --help');
199
+ fail('✕ --bell cannot be combined with --mention or --no-mention\nsquare express --help');
198
200
  if (noMention && mentions.length > 0)
199
- fail('✕ --no-mention cannot be combined with --mention\n» square express --help');
201
+ fail('✕ --no-mention cannot be combined with --mention\nsquare express --help');
200
202
  if (!bell && !noMention && mentions.length === 0)
201
- fail('✕ express needs --mention <name>, --no-mention, or --bell\n» square express --help');
203
+ fail('✕ express needs --mention <name>, --no-mention, or --bell\nsquare express --help');
202
204
  if (bodyArgs.length !== 1) {
203
205
  if (bodyArgs.length === 0) {
204
206
  if (!process.stdin.isTTY)
@@ -320,7 +322,7 @@ export const doneCommand = {
320
322
  const name = result.activity.actor;
321
323
  const presentation = await openSquare(squarePath, { clock: nowMs });
322
324
  const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
323
- return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
325
+ return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · ${result.activity.id} · just now`, { participantCount });
324
326
  },
325
327
  present: (result) => process.stdout.write(result),
326
328
  };
@@ -341,7 +343,7 @@ export const holdCommand = {
341
343
  const presentationSquare = await openSquare(squarePath, { clock: nowMs });
342
344
  const presentation = await eventPresentation(presentationSquare, result.activity.id);
343
345
  await closeOpenSquare(presentationSquare);
344
- return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
346
+ return withPathOutput(squarePath, `· ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount, held: true });
345
347
  }
346
348
  finally {
347
349
  await square.close();
@@ -365,7 +367,7 @@ export const resumeCommand = {
365
367
  const presentationSquare = await openSquare(squarePath, { clock: nowMs });
366
368
  const presentation = await eventPresentation(presentationSquare, result.activity.id);
367
369
  await closeOpenSquare(presentationSquare);
368
- return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
370
+ return withPathOutput(squarePath, `✓ ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount });
369
371
  }
370
372
  finally {
371
373
  await square.close();
@@ -58,6 +58,8 @@ export type ActDecision = {
58
58
  type: 'bell_quota';
59
59
  nextAt: number;
60
60
  };
61
+ /** The unread activity a viewer has not caught up to, grouped by speaker. */
62
+ export declare function unreadActivitySummaries(squareState: SquareState, name: string, now: number): UnreadActivitySummary[];
61
63
  export declare function decideAct(squareState: SquareState, input: {
62
64
  name: string;
63
65
  body: string;
package/dist/decisions.js CHANGED
@@ -9,7 +9,7 @@ export function resolveKnownName(squareState, name) {
9
9
  validateName(name);
10
10
  const known = resolveRosterName(squareState, name);
11
11
  if (known === undefined) {
12
- throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(name)}".`);
12
+ throw new SquareError('invalid_args', `✕ ${participantIdentity(name)} has never stepped into this square`);
13
13
  }
14
14
  return known;
15
15
  }
@@ -66,6 +66,40 @@ function requireStanding(squareState, act) {
66
66
  throw new Error(`Unexpected standing validation result: ${result.reason}`);
67
67
  }
68
68
  const UNREAD_PREVIEW_LIMIT = 3;
69
+ /** The unread activity a viewer has not caught up to, grouped by speaker. */
70
+ export function unreadActivitySummaries(squareState, name, now) {
71
+ const delivery = deriveDeliveryModel(squareState);
72
+ const delta = actDelta(squareState.acts, delivery.cursorFor(name));
73
+ const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
74
+ const sayCountByActor = new Map();
75
+ const unreadByParticipant = new Map();
76
+ for (const item of unreadPublic) {
77
+ if (item.kind === 'say') {
78
+ const key = item.actor.toLocaleLowerCase();
79
+ sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
80
+ }
81
+ if (sameName(item.actor, name))
82
+ continue;
83
+ const actorKey = item.actor.toLocaleLowerCase();
84
+ const currentSummary = unreadByParticipant.get(item.actor);
85
+ unreadByParticipant.set(item.actor, {
86
+ count: (currentSummary?.count ?? 0) + 1,
87
+ latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
88
+ previews: [
89
+ ...(currentSummary?.previews ?? []),
90
+ { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
91
+ ].slice(-UNREAD_PREVIEW_LIMIT),
92
+ });
93
+ }
94
+ return [...unreadByParticipant.entries()]
95
+ .map(([participant, summary]) => ({
96
+ name: participant,
97
+ count: summary.count,
98
+ latestActivityAgeMs: Math.max(0, now - summary.latestAt),
99
+ previews: summary.previews,
100
+ }))
101
+ .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
102
+ }
69
103
  export function decideAct(squareState, input) {
70
104
  const { now, force } = input;
71
105
  const name = resolveStandingName(squareState, input.name);
@@ -81,7 +115,7 @@ export function decideAct(squareState, input) {
81
115
  validateName(requested);
82
116
  const resolved = joinedNames.find((candidate) => sameName(candidate, requested));
83
117
  if (resolved === undefined) {
84
- throw new SquareError('invalid_args', `Unknown mention target ${participantIdentity(requested)}.`);
118
+ throw new SquareError('invalid_args', `✕ ${participantIdentity(requested)} is not standing in this square`);
85
119
  }
86
120
  if (!mentionNames.some((existing) => sameName(existing, resolved))) {
87
121
  if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
@@ -126,34 +160,7 @@ export function decideAct(squareState, input) {
126
160
  const delta = actDelta(squareState.acts, delivery.cursorFor(name));
127
161
  const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
128
162
  const unreadRoomChanges = [];
129
- const sayCountByActor = new Map();
130
- const unreadByParticipant = new Map();
131
- for (const item of unreadPublic) {
132
- if (item.kind === 'say') {
133
- const key = item.actor.toLocaleLowerCase();
134
- sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
135
- }
136
- if (sameName(item.actor, name))
137
- continue;
138
- const actorKey = item.actor.toLocaleLowerCase();
139
- const currentSummary = unreadByParticipant.get(item.actor);
140
- unreadByParticipant.set(item.actor, {
141
- count: (currentSummary?.count ?? 0) + 1,
142
- latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
143
- previews: [
144
- ...(currentSummary?.previews ?? []),
145
- { number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
146
- ].slice(-UNREAD_PREVIEW_LIMIT),
147
- });
148
- }
149
- const activitySummaries = [...unreadByParticipant.entries()]
150
- .map(([participant, summary]) => ({
151
- name: participant,
152
- count: summary.count,
153
- latestActivityAgeMs: Math.max(0, now - summary.latestAt),
154
- previews: summary.previews,
155
- }))
156
- .sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
163
+ const activitySummaries = unreadActivitySummaries(squareState, name, now);
157
164
  const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
158
165
  const hasUnread = unreadPublic.length > 0;
159
166
  const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
@@ -168,7 +175,7 @@ export function decideAct(squareState, input) {
168
175
  ...(reach !== undefined ? { reach } : {}),
169
176
  ...(reply !== undefined ? { reply } : {}),
170
177
  },
171
- confirmation: `● heads turn your way — #${ownActCount}`,
178
+ confirmation: `● your activity lands — #${ownActCount}`,
172
179
  ownActCount,
173
180
  pendingPublic: unreadPublic,
174
181
  pendingRoomChanges: unreadRoomChanges,
package/dist/list.js CHANGED
@@ -5,6 +5,7 @@ import { closeOpenSquare } from './open-square.js';
5
5
  import { listPresentation } from './views.js';
6
6
  import { formatRelativeTime } from './time.js';
7
7
  import { participantIdentity, quoteShell } from './presentation.js';
8
+ import { style } from './tty-style.js';
8
9
  const DEFAULT_LIST_DEPTH = 4;
9
10
  const MAX_LIST_DEPTH = 16;
10
11
  const DEFAULT_LIST_LIMIT = 20;
@@ -84,35 +85,37 @@ function renderSquareList(items, stopped, depth, limit, after) {
84
85
  const page = after === undefined ? items : items.filter((item) => item.path.localeCompare(after) > 0);
85
86
  const shownItems = page.slice(0, limit);
86
87
  if (items.length === 0) {
87
- return [`(no squares found)`, ...(stopped ? [`○ discovery stopped after examining ${LIST_DISCOVERY_BUDGET} filesystem entries; results may be incomplete`] : [])].join('\n') + '\n';
88
+ return [`(no squares found)`, ...(stopped ? [style('dim', `○ the walk stopped early — deeper squares may still be hiding`)] : [])].join('\n') + '\n';
88
89
  }
89
90
  const now = Date.now();
90
91
  const lines = ['squares'];
91
92
  for (const item of shownItems) {
92
- lines.push(`${item.activities > 0 ? '●' : '○'} ${characterPreview(item.path)} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants.length} in square · ${item.activities} activities`);
93
+ const glyph = item.activities > 0 ? '●' : style('dim', '○');
94
+ lines.push(`${glyph} ${characterPreview(item.path)}${style('dim', ` · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants.length} in the square · ${item.activities} ${item.activities === 1 ? 'activity' : 'activities'}`)}`);
93
95
  const shownContext = item.context.slice(0, CONTEXT_PREVIEW_LINES);
94
96
  if (shownContext.length === 0) {
95
- lines.push(' context · (none)');
97
+ lines.push(` ${style('dim', 'context')} · (none)`);
96
98
  }
97
99
  else {
98
- shownContext.forEach((line, index) => lines.push(` ${index === 0 ? 'context' : ' '} · ${characterPreview(line)}`));
100
+ shownContext.forEach((line, index) => lines.push(` ${index === 0 ? style('dim', 'context') : ' '} · ${characterPreview(line)}`));
99
101
  const hiddenContext = item.context.length - shownContext.length;
100
102
  if (hiddenContext > 0)
101
- lines.push(` · … ${hiddenContext} more ${hiddenContext === 1 ? 'line' : 'lines'}`);
103
+ lines.push(style('dim', ` · … ${hiddenContext} more ${hiddenContext === 1 ? 'line' : 'lines'}`));
102
104
  }
103
105
  const shownParticipants = item.participants.slice(0, PARTICIPANT_PREVIEW_COUNT);
104
106
  const hiddenParticipants = item.participants.length - shownParticipants.length;
105
- lines.push(` participants · ${shownParticipants.length === 0 ? 'nobody' : shownParticipants.map((participant) => participantIdentity(characterPreview(participant))).join(' · ')}${hiddenParticipants > 0 ? ` · … ${hiddenParticipants} more` : ''}`);
107
+ const participantNames = shownParticipants.length === 0 ? 'nobody' : shownParticipants.map((participant) => participantIdentity(characterPreview(participant))).join(' · ');
108
+ lines.push(` ${style('dim', 'participants')} · ${participantNames}${hiddenParticipants > 0 ? style('dim', ` · … ${hiddenParticipants} more`) : ''}`);
106
109
  }
107
110
  const hiddenItems = page.length - shownItems.length;
108
111
  if (hiddenItems > 0) {
109
112
  const cursor = shownItems.at(-1)?.path;
110
- lines.push(stopped ? ' … more squares' : ` … ${hiddenItems} more ${hiddenItems === 1 ? 'square' : 'squares'}`);
113
+ lines.push(style('dim', stopped ? ' … more squares' : ` … ${hiddenItems} more ${hiddenItems === 1 ? 'square' : 'squares'}`));
111
114
  if (cursor !== undefined)
112
- lines.push(`» square list --depth ${depth} --limit ${limit} --after ${quoteShell(cursor)}`);
115
+ lines.push(`square list --depth ${depth} --limit ${limit} --after ${quoteShell(cursor)}`);
113
116
  }
114
117
  if (stopped)
115
- lines.push(`○ discovery stopped after examining ${LIST_DISCOVERY_BUDGET} filesystem entries; results may be incomplete`);
118
+ lines.push(style('dim', `○ the walk stopped early — deeper squares may still be hiding`));
116
119
  return lines.join('\n') + '\n';
117
120
  }
118
121
  function parseListOptions(args, usage) {
@@ -6,6 +6,8 @@ export type WatchStatus = 'stale' | 'empty-now' | 'quorum' | 'capped';
6
6
  interface HeaderOptions {
7
7
  participantCount?: number;
8
8
  held?: boolean;
9
+ /** Refusal bodies go to stderr, which stays literal even when it is a TTY. */
10
+ plain?: boolean;
9
11
  }
10
12
  interface ParticipantOutputOptions {
11
13
  squarePath: string;
@@ -46,6 +48,11 @@ interface WatchStatusOptions extends ParticipantOutputOptions {
46
48
  }
47
49
  export declare function displayPath(squarePath: string, cwd?: string): string;
48
50
  export declare function withPathOutput(squarePath: string, body?: string, opts?: HeaderOptions): string;
51
+ /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
52
+ export declare function formatRefusal(squarePath: string, bodyLines: string[], opts?: {
53
+ participantCount?: number;
54
+ held?: boolean;
55
+ }): string;
49
56
  export declare function quoteShell(value: string): string;
50
57
  export declare function commandPrefix(squarePath: string): string;
51
58
  export declare function participantCommandPrefix(squarePath: string, name: string): string;