@astrosheep/square 0.3.14 → 0.3.16

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 (62) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  2. package/dist/activity-feed.d.ts +2 -3
  3. package/dist/activity-feed.js +6 -15
  4. package/dist/activity.js +1 -1
  5. package/dist/artifact.js +17 -26
  6. package/dist/attention-presentation.d.ts +13 -0
  7. package/dist/attention-presentation.js +22 -0
  8. package/dist/automatic-session.js +6 -8
  9. package/dist/boundary-presentation.js +35 -14
  10. package/dist/claude-hook.js +5 -3
  11. package/dist/cli/context.d.ts +4 -4
  12. package/dist/cli/context.js +25 -4
  13. package/dist/cli/harness-command.js +1 -1
  14. package/dist/cli/maintenance-commands.js +5 -4
  15. package/dist/cli/observation-commands.js +24 -16
  16. package/dist/cli/program.js +1 -1
  17. package/dist/cli/registry.js +4 -1
  18. package/dist/cli/square-commands.d.ts +7 -0
  19. package/dist/cli/square-commands.js +123 -27
  20. package/dist/codex-hook.js +5 -3
  21. package/dist/decisions.d.ts +19 -0
  22. package/dist/decisions.js +45 -12
  23. package/dist/delivery.d.ts +14 -21
  24. package/dist/delivery.js +40 -74
  25. package/dist/help.js +8 -5
  26. package/dist/inbox.js +1 -0
  27. package/dist/index.d.ts +1 -10
  28. package/dist/index.js +0 -32
  29. package/dist/landing.d.ts +12 -0
  30. package/dist/landing.js +35 -4
  31. package/dist/model.d.ts +8 -11
  32. package/dist/model.js +2 -2
  33. package/dist/notifications.js +35 -11
  34. package/dist/paseo-delivery.js +7 -4
  35. package/dist/paseo.d.ts +6 -0
  36. package/dist/paseo.js +5 -0
  37. package/dist/presence.d.ts +3 -0
  38. package/dist/presence.js +40 -10
  39. package/dist/presentation.d.ts +6 -2
  40. package/dist/presentation.js +22 -9
  41. package/dist/presented.d.ts +2 -0
  42. package/dist/presented.js +20 -0
  43. package/dist/registry.js +4 -1
  44. package/dist/routes.d.ts +5 -0
  45. package/dist/routes.js +17 -0
  46. package/dist/runtime.d.ts +4 -5
  47. package/dist/runtime.js +35 -18
  48. package/dist/square-core.d.ts +17 -0
  49. package/dist/square-core.js +51 -1
  50. package/dist/square-facade.d.ts +9 -1
  51. package/dist/square-wiring.d.ts +8 -0
  52. package/dist/square-wiring.js +32 -3
  53. package/dist/views.d.ts +9 -3
  54. package/dist/views.js +16 -14
  55. package/dist/wake-port.d.ts +4 -1
  56. package/dist/wake-port.js +5 -0
  57. package/dist/wakes.js +3 -5
  58. package/dist/watch.js +1 -0
  59. package/package.json +19 -3
  60. package/skills/brainstorm/SKILL.md +1 -1
  61. package/skills/square/.claude-plugin/plugin.json +1 -1
  62. package/skills/square/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -4,11 +4,10 @@ export interface ActivityFeedFilter {
4
4
  mention?: string;
5
5
  }
6
6
  export declare function actDelta(acts: StoredAct[], cursor: number): StoredAct[];
7
- /** Public cursor changes plus directed receipts that remain pending behind it. */
7
+ /** Visible activities after the participant's derived continuous-seen prefix. */
8
8
  export declare function deliveryDelta(squareState: SquareState, name: string): StoredAct[];
9
9
  export declare function peerRoomChanges(delta: StoredAct[], name: string): RoomChangeAct[];
10
10
  export declare function peerPublicActs(delta: StoredAct[], name: string): PublicAct[];
11
- export declare function matchesFeedFilter(act: StoredAct, filter: ActivityFeedFilter): boolean;
11
+ export declare function matchesFeedFilter(act: StoredAct, filter: ActivityFeedFilter, recipients?: readonly string[]): boolean;
12
12
  export declare function filteredPeerActivities(delta: StoredAct[], name: string, filter: ActivityFeedFilter): PublicAct[];
13
13
  export declare function filteredRoomChanges(delta: StoredAct[], name: string, filter: ActivityFeedFilter): RoomChangeAct[];
14
- export declare function ackPeerDelta(squareState: SquareState, name: string, delta: StoredAct[], at?: number): boolean;
@@ -1,18 +1,12 @@
1
1
  import { sameName } from './model.js';
2
- import { advanceCursor, latestActIndex, readCursor } from './runtime.js';
3
- import { deriveDeliveryModel, matchesCatchFilter } from './delivery.js';
2
+ import { readCursor } from './runtime.js';
3
+ import { matchesCatchFilter } from './delivery.js';
4
4
  export function actDelta(acts, cursor) {
5
5
  return acts.filter((act) => act.index > cursor);
6
6
  }
7
- /** Public cursor changes plus directed receipts that remain pending behind it. */
7
+ /** Visible activities after the participant's derived continuous-seen prefix. */
8
8
  export function deliveryDelta(squareState, name) {
9
- const items = actDelta(squareState.acts, readCursor(squareState, name));
10
- const seen = new Set(items.map((act) => act.index));
11
- for (const notification of deriveDeliveryModel(squareState).pendingFor(name)) {
12
- if (!seen.has(notification.item.index))
13
- items.push(notification.item);
14
- }
15
- return items.sort((a, b) => a.index - b.index);
9
+ return actDelta(squareState.acts, readCursor(squareState, name));
16
10
  }
17
11
  export function peerRoomChanges(delta, name) {
18
12
  return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && act.kind !== 'say' && act.kind !== 'read');
@@ -23,9 +17,9 @@ export function peerPublicActs(delta, name) {
23
17
  function matchesParticipants(act, participants) {
24
18
  return participants === undefined || (act.actor !== undefined && participants.some((participant) => sameName(participant, act.actor)));
25
19
  }
26
- export function matchesFeedFilter(act, filter) {
20
+ export function matchesFeedFilter(act, filter, recipients) {
27
21
  if (act.kind === 'say') {
28
- return matchesCatchFilter({ actor: act.actor, body: act.body, reach: act.reach }, filter);
22
+ return matchesCatchFilter({ actor: act.actor, body: act.body, reach: act.reach, ...(recipients === undefined ? {} : { recipients }) }, filter);
29
23
  }
30
24
  return filter.mention === undefined && matchesParticipants(act, filter.participants);
31
25
  }
@@ -39,6 +33,3 @@ export function filteredRoomChanges(delta, name, filter) {
39
33
  return [];
40
34
  return peerRoomChanges(delta, name).filter((act) => matchesParticipants(act, filter.participants));
41
35
  }
42
- export function ackPeerDelta(squareState, name, delta, at) {
43
- return advanceCursor(squareState, name, latestActIndex([...peerPublicActs(delta, name), ...peerRoomChanges(delta, name)]), at);
44
- }
package/dist/activity.js CHANGED
@@ -87,7 +87,7 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
87
87
  const held = fresh.held;
88
88
  const ownActCount = fresh.ownActivityCount;
89
89
  const hasPending = pendingPublic.length > 0 || pendingRoomChanges.length > 0;
90
- const pending = hasPending ? `\n\n${renderPendingFeed([...fresh.activities], [...pendingPublic], [...pendingRoomChanges], knownName)}` : '';
90
+ const pending = hasPending ? `\n\n${renderPendingFeed([...fresh.activities], [...pendingPublic], [...pendingRoomChanges], knownName, fresh.state)}` : '';
91
91
  const hint = expressHintLine(ownActCount);
92
92
  const confirmation = `● heads turn your way — #${ownActCount}`;
93
93
  const withHint = hint ? `${confirmation}\n${hint}` : confirmation;
package/dist/artifact.js CHANGED
@@ -33,18 +33,12 @@ function isNonblankString(value) {
33
33
  function isStringArray(value) {
34
34
  return Array.isArray(value) && value.every((item) => typeof item === 'string');
35
35
  }
36
- function validateReadCursor(value) {
36
+ function validateObservation(value) {
37
37
  return isObject(value)
38
- && hasExactKeys(value, ['consumedThroughIndex', 'updatedAt'])
39
- && Number.isSafeInteger(value.consumedThroughIndex)
40
- && value.consumedThroughIndex >= -1
41
- && isFiniteNumber(value.updatedAt);
42
- }
43
- function validateDeliveryReceipt(value) {
44
- return isObject(value)
45
- && hasExactKeys(value, ['status', 'at'])
46
- && value.status === 'delivered'
47
- && isFiniteNumber(value.at);
38
+ && hasExactKeys(value, ['state', 'at'], ['ownerId'])
39
+ && (value.state === 'notified' || value.state === 'seen')
40
+ && isFiniteNumber(value.at)
41
+ && (value.ownerId === undefined || isNonblankString(value.ownerId));
48
42
  }
49
43
  function validateWatchLease(value) {
50
44
  if (!isObject(value)
@@ -100,25 +94,18 @@ function parseNotifyLeaseKey(key) {
100
94
  }
101
95
  function validateRuntime(value) {
102
96
  if (!isObject(value)
103
- || !hasExactKeys(value, ['nextActIndex', 'cursors', 'deliveryReceipts', 'leases', 'notifyLeases'])
97
+ || !hasExactKeys(value, ['nextActIndex', 'observations', 'leases', 'notifyLeases'])
104
98
  || !isNonNegativeInteger(value.nextActIndex)
105
- || !validateRecord(value.cursors, validateReadCursor)
99
+ || !validateRecord(value.observations, (candidate) => isObject(candidate) && Object.entries(candidate).every(([id, observation]) => parseActivityId(id) !== undefined && validateObservation(observation)))
106
100
  || !validateRecord(value.leases, validateWatchLease)
107
- || !validateRecord(value.notifyLeases, validateNotifyLease)
108
- || !isObject(value.deliveryReceipts))
101
+ || !validateRecord(value.notifyLeases, validateNotifyLease))
109
102
  return false;
110
- return Object.entries(value.deliveryReceipts).every(([name, receipts]) => name.length > 0
111
- && isObject(receipts)
112
- && Object.entries(receipts).every(([id, receipt]) => parseActivityId(id) !== undefined && validateDeliveryReceipt(receipt)));
103
+ return true;
113
104
  }
114
105
  function validateAssignedRuntimeReferences(runtime) {
115
106
  const bound = runtime.nextActIndex;
116
- for (const cursor of Object.values(runtime.cursors)) {
117
- if (cursor.consumedThroughIndex !== -1 && cursor.consumedThroughIndex >= bound)
118
- return 'future';
119
- }
120
- for (const receipts of Object.values(runtime.deliveryReceipts)) {
121
- for (const id of Object.keys(receipts)) {
107
+ for (const observations of Object.values(runtime.observations)) {
108
+ for (const id of Object.keys(observations)) {
122
109
  const index = parseActivityId(id);
123
110
  if (index === undefined)
124
111
  return 'malformed';
@@ -167,6 +154,11 @@ function validateStoredAct(value) {
167
154
  return hasExactKeys(value, ['kind', 'actor', 'at', 'through', 'index'])
168
155
  && validateActor(value.actor, true)
169
156
  && isNonNegativeInteger(value.through);
157
+ case 'listen':
158
+ case 'ignore':
159
+ return hasExactKeys(value, ['kind', 'actor', 'target', 'at', 'index'])
160
+ && validateActor(value.actor, true)
161
+ && validateActor(value.target, true);
170
162
  default:
171
163
  return false;
172
164
  }
@@ -257,8 +249,7 @@ function decodeEnvelope(bytes, magic) {
257
249
  export function emptyRuntimeState(nextActIndex = 0) {
258
250
  return {
259
251
  nextActIndex,
260
- cursors: {},
261
- deliveryReceipts: {},
252
+ observations: {},
262
253
  leases: {},
263
254
  notifyLeases: {},
264
255
  };
@@ -0,0 +1,13 @@
1
+ import type { DirectedNotificationRoute } from './model.js';
2
+ export declare const ATTENTION_BODY_MAX = 120;
3
+ export interface AttentionPreview {
4
+ squarePath: string;
5
+ actIndex: number;
6
+ recipient: string;
7
+ actor: string;
8
+ route: DirectedNotificationRoute;
9
+ body: string;
10
+ }
11
+ export declare function previewAttentionBody(body: string): string;
12
+ export declare function displayAttentionPath(squarePath: string): string;
13
+ export declare function renderAttentionPreview(attention: AttentionPreview): string;
@@ -0,0 +1,22 @@
1
+ import { homedir } from 'node:os';
2
+ import { notificationMessageId } from './delivery.js';
3
+ import { participantIdentity } from './presentation.js';
4
+ export const ATTENTION_BODY_MAX = 120;
5
+ export function previewAttentionBody(body) {
6
+ const compact = body.replace(/\r\n/g, '\n');
7
+ if (compact.length <= ATTENTION_BODY_MAX)
8
+ return compact;
9
+ return `${compact.slice(0, ATTENTION_BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
10
+ }
11
+ export function displayAttentionPath(squarePath) {
12
+ return squarePath.startsWith(homedir())
13
+ ? `~${squarePath.slice(homedir().length)}`
14
+ : squarePath;
15
+ }
16
+ export function renderAttentionPreview(attention) {
17
+ const attentionKind = attention.route === 'bell' ? 'bell' : 'attention';
18
+ return [
19
+ `${notificationMessageId(attention.squarePath, attention.actIndex)} · ${displayAttentionPath(attention.squarePath)}: ${participantIdentity(attention.recipient)} from ${participantIdentity(attention.actor)} (${attentionKind})`,
20
+ previewAttentionBody(attention.body),
21
+ ].join('\n');
22
+ }
@@ -5,7 +5,7 @@ import { openSquare } from './square-file-adapter.js';
5
5
  import { closeOpenSquare } from './open-square.js';
6
6
  import { Square } from './square-wiring.js';
7
7
  import { entryPresentation } from './views.js';
8
- import { canonicalSquarePath, lookupSession, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
8
+ import { canonicalSquarePath, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
9
9
  import { participantIdentity, renderAmbientEvent } from './presentation.js';
10
10
  import { validateName } from './model.js';
11
11
  const providerEnv = {
@@ -39,16 +39,13 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
39
39
  return undefined;
40
40
  }
41
41
  const name = automaticParticipant(provider, sessionId, env);
42
- const bindings = lookupSession(sessionId);
43
- const before = await entryPresentation(reader, name);
44
- if (bindings.some((binding) => canonicalSquarePath(binding.squarePath) === canonicalSquarePath(squarePath) && binding.name === name) && before.joined) {
45
- await closeOpenSquare(reader);
46
- return undefined;
47
- }
42
+ const alreadyBound = lookupSessionBindings(sessionId).some((binding) => canonicalSquarePath(binding.squarePath) === canonicalSquarePath(squarePath) && binding.name === name);
48
43
  await closeOpenSquare(reader);
49
44
  const square = await Square.at({ path: squarePath });
50
45
  try {
51
- await square.join(name);
46
+ const implicit = await square.implicitJoin(name);
47
+ if (implicit.state === 'done' || (implicit.state === 'active' && alreadyBound))
48
+ return undefined;
52
49
  const channel = provider === 'claude' ? 'claude-code' : provider;
53
50
  recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
54
51
  const afterSquare = await openSquare(squarePath);
@@ -57,6 +54,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
57
54
  now: Date.now(),
58
55
  preview: 200,
59
56
  actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
57
+ squareState: after.state,
60
58
  })).filter(Boolean).join('\n\n');
61
59
  return [`You joined the public square as ${participantIdentity(name)}.`, after.scene, after.context ? `context\n${after.context}` : '', activity ? `recent activity\n${activity}` : ''].filter(Boolean).join('\n\n');
62
60
  }
@@ -1,8 +1,10 @@
1
- import { leaseOwnsNotification, notificationMessageId } from './delivery.js';
1
+ import { leaseOwnsNotification } from './delivery.js';
2
2
  import { sessionInbox } from './inbox.js';
3
- import { participantCommandPrefix, participantIdentity } from './presentation.js';
3
+ import { notificationMessageId } from './delivery.js';
4
+ import { markBoundarySeen } from './square-wiring.js';
5
+ import { renderAttentionPreview } from './attention-presentation.js';
6
+ import { participantCommandPrefix } from './presentation.js';
4
7
  import { presentOnce } from './presented.js';
5
- const BODY_MAX = 200;
6
8
  const CONTEXT_MAX = 1200;
7
9
  function pendingCount(inbox) {
8
10
  return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
@@ -16,17 +18,11 @@ export function pendingAtBoundary(inbox) {
16
18
  return membership;
17
19
  return {
18
20
  ...membership,
19
- notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, notification)),
21
+ notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })),
20
22
  };
21
23
  })
22
24
  .filter((membership) => membership.notifications.length > 0);
23
25
  }
24
- function bodyPreview(body) {
25
- const compact = body.replace(/\r\n/g, '\n');
26
- if (compact.length <= BODY_MAX)
27
- return compact;
28
- return `${compact.slice(0, BODY_MAX).trimEnd()}\n… [truncated; run catch --now]`;
29
- }
30
26
  export function renderPendingAtBoundary(inbox) {
31
27
  const count = pendingCount(inbox);
32
28
  const noun = count === 1 ? 'notification' : 'notifications';
@@ -41,10 +37,15 @@ export function renderPendingAtBoundary(inbox) {
41
37
  for (const [index, entry] of queued.entries()) {
42
38
  const { membership, notification } = entry;
43
39
  const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
44
- const id = notificationMessageId(membership.squarePath, notification.actIndex);
45
40
  const block = [
46
- `${id} · ${membership.squarePath}: ${participantIdentity(membership.name)} from ${participantIdentity(notification.actor)} (${notification.route})`,
47
- bodyPreview(notification.body),
41
+ renderAttentionPreview({
42
+ squarePath: membership.squarePath,
43
+ actIndex: notification.actIndex,
44
+ recipient: membership.name,
45
+ actor: notification.actor,
46
+ route: notification.route,
47
+ body: notification.body,
48
+ }),
48
49
  `Ack with: ${command}`,
49
50
  ].join('\n');
50
51
  const omittedAfter = omitted + queued.length - index - 1;
@@ -74,5 +75,25 @@ export function renderPendingAtBoundary(inbox) {
74
75
  }
75
76
  export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env) {
76
77
  const inbox = await lookup(sessionId);
77
- return presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => present(renderPendingAtBoundary(inbox)), env);
78
+ let deliveredInbox;
79
+ let deliveredContext;
80
+ const result = presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => {
81
+ const context = renderPendingAtBoundary(inbox);
82
+ deliveredInbox = inbox;
83
+ deliveredContext = context;
84
+ return present(context);
85
+ }, env);
86
+ if (result !== undefined && deliveredInbox !== undefined && deliveredContext !== undefined) {
87
+ await markCompleteBoundaryObservations(deliveredInbox, deliveredContext);
88
+ }
89
+ return result;
90
+ }
91
+ async function markCompleteBoundaryObservations(inbox, context) {
92
+ for (const membership of inbox) {
93
+ const complete = membership.notifications
94
+ .filter((notification) => context.includes(notificationMessageId(membership.squarePath, notification.actIndex)) && notification.body.length <= 120)
95
+ .map((notification) => notification.actIndex);
96
+ if (complete.length > 0)
97
+ await markBoundarySeen(membership.squarePath, membership.name, membership.ownerId, complete);
98
+ }
78
99
  }
@@ -12,11 +12,12 @@ export async function runClaudeHookAsync(inputText, env = process.env) {
12
12
  if (input === null || typeof input !== 'object')
13
13
  return '';
14
14
  const value = input;
15
- if (typeof value.session_id !== 'string' || typeof value.cwd !== 'string')
15
+ if (typeof value.session_id !== 'string')
16
16
  return runClaudeHook(inputText, env);
17
17
  if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
18
+ const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
18
19
  try {
19
- const context = await automaticSessionStart('claude', value.session_id, value.cwd, env);
20
+ const context = await automaticSessionStart('claude', value.session_id, cwd, env);
20
21
  return context === undefined ? '' : `${JSON.stringify({ hookSpecificOutput: { hookEventName: value.hook_event_name, additionalContext: context } })}\n`;
21
22
  }
22
23
  catch {
@@ -24,8 +25,9 @@ export async function runClaudeHookAsync(inputText, env = process.env) {
24
25
  }
25
26
  }
26
27
  if (value.hook_event_name === 'SessionEnd') {
28
+ const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
27
29
  try {
28
- await automaticSessionEnd('claude', value.session_id, value.cwd, env);
30
+ await automaticSessionEnd('claude', value.session_id, cwd, env);
29
31
  }
30
32
  catch { /* end remains bounded */ }
31
33
  return '';
@@ -1,7 +1,6 @@
1
1
  import { type HardCap } from '../model.js';
2
- export declare const DEFAULT_SQUARE_PATH = ".square/SQUARE.square";
3
2
  export interface CommandContext {
4
- squarePath: string;
3
+ squarePath?: string;
5
4
  name?: string;
6
5
  homeDir: string;
7
6
  command: string;
@@ -24,7 +23,7 @@ export declare function parseHardCap(value: string): HardCap;
24
23
  export declare function parseNameList(value: string, flag: string): string[];
25
24
  export declare function requireParticipant(name: string | undefined): string;
26
25
  export interface ParsedGlobalArgs {
27
- squarePath: string;
26
+ squarePath?: string;
28
27
  explicitSquarePath: boolean;
29
28
  multipleSquares: boolean;
30
29
  name?: string;
@@ -32,4 +31,5 @@ export interface ParsedGlobalArgs {
32
31
  }
33
32
  export declare function parseGlobalArgs(rawArgs: string[]): ParsedGlobalArgs;
34
33
  export declare function locationIsRequired(command: string): boolean;
35
- export declare function defaultContext(command: string, squarePath: string, name?: string): CommandContext;
34
+ export declare function defaultContext(command: string, squarePath?: string, name?: string): CommandContext;
35
+ export declare function requireSquarePath(context: CommandContext): string;
@@ -3,7 +3,6 @@ import os from 'node:os';
3
3
  import { commandUsageHint } from '../help.js';
4
4
  import { parseParticipantList, validateName } from '../model.js';
5
5
  import { localParticipantName } from '../registry.js';
6
- export const DEFAULT_SQUARE_PATH = '.square/SQUARE.square';
7
6
  export function readStdinSync() {
8
7
  try {
9
8
  return fs.readFileSync(0, 'utf8');
@@ -87,7 +86,23 @@ export function requireParticipant(name) {
87
86
  validateName(name);
88
87
  return name;
89
88
  }
90
- const LOCATION_REQUIRED_COMMANDS = new Set(['join', 'catch', 'express', 'done', 'hold', 'resume']);
89
+ const LOCATION_REQUIRED_COMMANDS = new Set([
90
+ 'build',
91
+ 'stream',
92
+ 'join',
93
+ 'catch',
94
+ 'express',
95
+ 'listen',
96
+ 'ignore',
97
+ 'listening',
98
+ 'done',
99
+ 'hold',
100
+ 'resume',
101
+ 'history',
102
+ 'status',
103
+ 'participants',
104
+ 'doctor',
105
+ ]);
91
106
  function configuredLocation() {
92
107
  const value = process.env.SQUARE_LOCATION?.trim();
93
108
  return value === '' ? undefined : value;
@@ -125,9 +140,10 @@ export function parseGlobalArgs(rawArgs) {
125
140
  if (command !== undefined && !['--help', '-h'].includes(command) && locationIsRequired(command) && requestedPath === undefined && configured === undefined) {
126
141
  fail(`✕ ${command} needs a square location\n» square ls`);
127
142
  }
128
- const squarePath = requestedPath ?? configured ?? DEFAULT_SQUARE_PATH;
129
- if (name === undefined && command !== undefined && locationIsRequired(command))
143
+ const squarePath = requestedPath ?? configured;
144
+ if (name === undefined && squarePath !== undefined && command !== undefined && locationIsRequired(command)) {
130
145
  name = localParticipantName(squarePath);
146
+ }
131
147
  return { squarePath, explicitSquarePath: explicitSquarePath || configured !== undefined, multipleSquares: false, name, args };
132
148
  }
133
149
  export function locationIsRequired(command) {
@@ -136,3 +152,8 @@ export function locationIsRequired(command) {
136
152
  export function defaultContext(command, squarePath, name) {
137
153
  return { command, squarePath, name, homeDir: os.homedir() };
138
154
  }
155
+ export function requireSquarePath(context) {
156
+ if (context.squarePath === undefined)
157
+ fail(`✕ ${context.command} needs a square location\n» square ls`);
158
+ return context.squarePath;
159
+ }
@@ -102,7 +102,7 @@ export function formatHarnessResult(result) {
102
102
  return `${[...result.notes, ...result.lines, ...result.failures].join('\n')}\n`;
103
103
  }
104
104
  export function runHarnessCommand(argv, squarePath) {
105
- const context = { homeDir: os.homedir(), squarePath: squarePath ?? '.square/SQUARE.square', command: 'harness' };
105
+ const context = { homeDir: os.homedir(), ...(squarePath === undefined ? {} : { squarePath }), command: 'harness' };
106
106
  const intent = harnessCommand.parse(argv, context);
107
107
  return Promise.resolve(harnessCommand.execute(intent, context)).then(formatHarnessResult);
108
108
  }
@@ -1,7 +1,7 @@
1
1
  import { diagnoseSquareFile } from '../square-storage.js';
2
2
  import { renderDoctorClean, renderDoctorUnfixable, withPathOutput } from '../presentation.js';
3
3
  import { inSquareCount } from '../runtime.js';
4
- import { usage } from './context.js';
4
+ import { requireSquarePath, usage } from './context.js';
5
5
  export const doctorCommand = {
6
6
  parse(argv, context) {
7
7
  if (argv.length > 0)
@@ -9,15 +9,16 @@ export const doctorCommand = {
9
9
  return undefined;
10
10
  },
11
11
  execute(_intent, context) {
12
- const diagnosis = diagnoseSquareFile(context.squarePath);
12
+ const squarePath = requireSquarePath(context);
13
+ const diagnosis = diagnoseSquareFile(squarePath);
13
14
  if (diagnosis.unfixable !== undefined || diagnosis.state === undefined) {
14
15
  return {
15
- output: withPathOutput(context.squarePath, renderDoctorUnfixable(diagnosis.unfixable ?? 'the snapshot could not be decoded')),
16
+ output: withPathOutput(squarePath, renderDoctorUnfixable(diagnosis.unfixable ?? 'the snapshot could not be decoded')),
16
17
  exitCode: 2,
17
18
  };
18
19
  }
19
20
  return {
20
- output: withPathOutput(context.squarePath, renderDoctorClean(), {
21
+ output: withPathOutput(squarePath, renderDoctorClean(), {
21
22
  participantCount: inSquareCount(diagnosis.state),
22
23
  }),
23
24
  };
@@ -12,7 +12,7 @@ import { cmdWatch } from '../watch.js';
12
12
  import { openSquare } from '../square-file-adapter.js';
13
13
  import { closeOpenSquare } from '../open-square.js';
14
14
  import { historyPresentation, participantsPresentation, statusPresentation } from '../views.js';
15
- import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
15
+ import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
16
16
  export const listCommand = {
17
17
  parse: (argv) => argv,
18
18
  async execute(argv, context) {
@@ -39,10 +39,11 @@ export const streamCommand = {
39
39
  return { ndjson, forName };
40
40
  },
41
41
  async execute(intent, context) {
42
+ const squarePath = requireSquarePath(context);
42
43
  if (intent.ndjson)
43
- await cmdStreamNdjson(context.squarePath, intent.forName);
44
+ await cmdStreamNdjson(squarePath, intent.forName);
44
45
  else
45
- await cmdStream(context.squarePath);
46
+ await cmdStream(squarePath);
46
47
  },
47
48
  present: () => { },
48
49
  };
@@ -93,8 +94,9 @@ export const catchCommand = {
93
94
  };
94
95
  },
95
96
  async execute(intent, context) {
96
- await cmdWatch(context.squarePath, requireParticipant(context.name), intent);
97
- await sweepPendingNotifications(context.squarePath);
97
+ const squarePath = requireSquarePath(context);
98
+ await cmdWatch(squarePath, requireParticipant(context.name), intent);
99
+ await sweepPendingNotifications(squarePath);
98
100
  },
99
101
  present: () => { },
100
102
  };
@@ -111,6 +113,7 @@ function parseTimestamp(value, flag) {
111
113
  return timestamp;
112
114
  }
113
115
  function parseHistory(argv, context) {
116
+ const squarePath = requireSquarePath(context);
114
117
  const viewer = context.name;
115
118
  let lastN = 10;
116
119
  let lastNExplicit = false;
@@ -133,7 +136,7 @@ function parseHistory(argv, context) {
133
136
  const flag = argv[index];
134
137
  if (flag === '--limit') {
135
138
  const value = argv[index + 1];
136
- const retry = `${commandPrefix(context.squarePath)} history --limit 30`;
139
+ const retry = `${commandPrefix(squarePath)} history --limit 30`;
137
140
  if (value === undefined || value.startsWith('--'))
138
141
  fail(`✕ --limit needs a positive number\n» ${retry}`);
139
142
  if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
@@ -283,6 +286,7 @@ function renderHistoryProjection(projection, visible, full, squarePath, viewer,
283
286
  const options = {
284
287
  preview,
285
288
  actNumber: activity.kind === 'say' ? projection.sayNumbers[activity.index] : undefined,
289
+ perception: activity.perception,
286
290
  };
287
291
  const rendered = mode === 'archive'
288
292
  ? renderEventCli(activity, options)
@@ -302,7 +306,8 @@ function renderHistoryProjection(projection, visible, full, squarePath, viewer,
302
306
  export const historyCommand = {
303
307
  parse(argv, context) { return parseHistory(argv, context); },
304
308
  async execute(options, context) {
305
- const square = await openSquare(context.squarePath, { clock: nowMs });
309
+ const squarePath = requireSquarePath(context);
310
+ const square = await openSquare(squarePath, { clock: nowMs });
306
311
  try {
307
312
  const projection = await historyPresentation(square, options);
308
313
  let events = [...projection.activities];
@@ -324,9 +329,9 @@ export const historyCommand = {
324
329
  || (options.lastN == null && options.full === true)
325
330
  || anonymous;
326
331
  const output = pattern === undefined || pattern === ''
327
- ? renderHistoryProjection(projection, events, options.full === true || anonymous, context.squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
328
- : renderGrepActivitiesView(events, totalMatches, options.full, context.squarePath, pattern, options.fixed !== undefined);
329
- return withPathOutput(context.squarePath, output, { participantCount: projection.participantCount });
332
+ ? renderHistoryProjection(projection, events, options.full === true || anonymous, squarePath, options.viewer ?? '', archive ? 'archive' : 'ambient')
333
+ : renderGrepActivitiesView(events, totalMatches, options.full, squarePath, pattern, options.fixed !== undefined);
334
+ return withPathOutput(squarePath, output, { participantCount: projection.participantCount });
330
335
  }
331
336
  finally {
332
337
  await closeOpenSquare(square);
@@ -338,7 +343,8 @@ export const participantsCommand = {
338
343
  parse(argv, context) { if (argv.length > 0)
339
344
  usage(context.command); return undefined; },
340
345
  async execute(_intent, context) {
341
- const square = await openSquare(context.squarePath, { clock: nowMs });
346
+ const squarePath = requireSquarePath(context);
347
+ const square = await openSquare(squarePath, { clock: nowMs });
342
348
  try {
343
349
  const now = nowMs();
344
350
  const participants = await participantsPresentation(square);
@@ -349,7 +355,7 @@ export const participantsCommand = {
349
355
  return ` ${glyph} ${participantIdentity(participant.name)} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
350
356
  });
351
357
  const participantCount = participants.filter((participant) => participant.state === 'active').length;
352
- return withPathOutput(context.squarePath, ['participants', ...lines].join('\n'), {
358
+ return withPathOutput(squarePath, ['participants', ...lines].join('\n'), {
353
359
  participantCount,
354
360
  });
355
361
  }
@@ -363,7 +369,8 @@ export const statusCommand = {
363
369
  parse(argv, context) { if (argv.length > 0)
364
370
  usage(context.command); return undefined; },
365
371
  async execute(_intent, context) {
366
- const square = await openSquare(context.squarePath, { clock: nowMs });
372
+ const squarePath = requireSquarePath(context);
373
+ const square = await openSquare(squarePath, { clock: nowMs });
367
374
  try {
368
375
  const presentation = await statusPresentation(square);
369
376
  const result = presentation.status;
@@ -387,7 +394,7 @@ export const statusCommand = {
387
394
  const attention = !showAttention
388
395
  ? ''
389
396
  : participant.pendingMentionCount > 0
390
- ? `${participant.pendingMentionCount} mention${participant.pendingMentionCount === 1 ? '' : 's'} waiting`
397
+ ? `${participant.pendingMentionCount} attention${participant.pendingMentionCount === 1 ? '' : 's'} waiting`
391
398
  : participant.unreadActivityCount > 0
392
399
  ? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
393
400
  : 'caught up';
@@ -405,6 +412,7 @@ export const statusCommand = {
405
412
  now: result.now,
406
413
  preview: 200,
407
414
  actNumber: presentation.latestActNumber,
415
+ squareState: presentation.state,
408
416
  });
409
417
  const latest = visible === ''
410
418
  ? [result.latestAct === undefined
@@ -412,14 +420,14 @@ export const statusCommand = {
412
420
  : ' · latest activity is private to another participant']
413
421
  : [` ${visible.replace(/\n/g, '\n ')}`];
414
422
  if (visible.includes('more chars') && result.latestAct !== undefined) {
415
- const prefix = context.name === undefined ? commandPrefix(context.squarePath) : participantCommandPrefix(context.squarePath, context.name);
423
+ const prefix = context.name === undefined ? commandPrefix(squarePath) : participantCommandPrefix(squarePath, context.name);
416
424
  latest.push(`» ${prefix} history --at ${actId(result.latestAct)} -C 2 --full`);
417
425
  }
418
426
  const output = [
419
427
  `${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
420
428
  ...(hold === undefined ? [] : ['', hold]), '', 'around the square', ...people, '', 'latest', ...latest,
421
429
  ].join('\n');
422
- return withPathOutput(context.squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
430
+ return withPathOutput(squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
423
431
  }
424
432
  finally {
425
433
  await closeOpenSquare(square);
@@ -14,7 +14,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
14
14
  try {
15
15
  const requestedHelp = helpRequest(rawArgs);
16
16
  if (requestedHelp !== undefined) {
17
- await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help', '.square/SQUARE.square'));
17
+ await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help'));
18
18
  return;
19
19
  }
20
20
  const parsed = parseGlobalArgs(rawArgs);
@@ -1,4 +1,4 @@
1
- import { buildCommand, doneCommand, expressCommand, holdCommand, joinCommand, resumeCommand } from './square-commands.js';
1
+ import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand } from './square-commands.js';
2
2
  import { doctorCommand } from './maintenance-commands.js';
3
3
  import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
4
4
  import { helpCommand, versionCommand } from './meta-commands.js';
@@ -14,6 +14,9 @@ export const commandRegistry = [
14
14
  { names: ['codex-hook'], spec: codexHookCommand },
15
15
  { names: ['catch'], spec: catchCommand },
16
16
  { names: ['express'], spec: expressCommand },
17
+ { names: ['listen'], spec: listenCommand },
18
+ { names: ['ignore'], spec: ignoreCommand },
19
+ { names: ['listening'], spec: listeningCommand },
17
20
  { names: ['done'], spec: doneCommand },
18
21
  { names: ['hold'], spec: holdCommand },
19
22
  { names: ['resume'], spec: resumeCommand },
@@ -23,9 +23,16 @@ interface BodyIntent {
23
23
  name: string;
24
24
  body?: string;
25
25
  }
26
+ interface ListenerIntent {
27
+ name: string;
28
+ target?: string;
29
+ }
26
30
  export declare const buildCommand: CommandSpec<BuildIntent, string>;
27
31
  export declare const joinCommand: CommandSpec<JoinIntent, string>;
28
32
  export declare const expressCommand: CommandSpec<ActivityIntent>;
33
+ export declare const listenCommand: CommandSpec<ListenerIntent, string>;
34
+ export declare const ignoreCommand: CommandSpec<ListenerIntent, string>;
35
+ export declare const listeningCommand: CommandSpec<ListenerIntent, string>;
29
36
  export declare const doneCommand: CommandSpec<BodyIntent, string>;
30
37
  export declare const holdCommand: CommandSpec<BodyIntent, string>;
31
38
  export declare const resumeCommand: CommandSpec<{