@astrosheep/square 0.3.58 → 0.3.62

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 (91) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +5 -7
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity.d.ts +6 -2
  5. package/dist/activity.js +170 -133
  6. package/dist/artifact.d.ts +4 -0
  7. package/dist/artifact.js +43 -14
  8. package/dist/attention-presentation.d.ts +1 -0
  9. package/dist/attention-presentation.js +21 -19
  10. package/dist/automatic-session.js +16 -26
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/catch-decisions.js +1 -1
  14. package/dist/cli/context.d.ts +2 -0
  15. package/dist/cli/context.js +17 -33
  16. package/dist/cli/harness-command.js +1 -1
  17. package/dist/cli/mcp-server-command.d.ts +2 -0
  18. package/dist/cli/mcp-server-command.js +13 -0
  19. package/dist/cli/observation-commands.js +132 -64
  20. package/dist/cli/program.js +36 -9
  21. package/dist/cli/registry.js +2 -0
  22. package/dist/cli/square-commands.d.ts +1 -1
  23. package/dist/cli/square-commands.js +103 -133
  24. package/dist/decisions.d.ts +4 -0
  25. package/dist/decisions.js +82 -34
  26. package/dist/delivery-operations.d.ts +1 -2
  27. package/dist/delivery-operations.js +1 -4
  28. package/dist/file-changes.d.ts +12 -0
  29. package/dist/file-changes.js +235 -0
  30. package/dist/help.js +4 -8
  31. package/dist/host-ledger-file-adapter.d.ts +15 -13
  32. package/dist/host-ledger-file-adapter.js +131 -79
  33. package/dist/host-ledger-root.d.ts +2 -0
  34. package/dist/host-ledger-root.js +9 -0
  35. package/dist/host-ledger.d.ts +4 -24
  36. package/dist/inbox.d.ts +10 -5
  37. package/dist/inbox.js +152 -67
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.js +1 -0
  40. package/dist/list.js +12 -9
  41. package/dist/mcp.d.ts +7 -0
  42. package/dist/mcp.js +122 -0
  43. package/dist/mention-parse.d.ts +8 -0
  44. package/dist/mention-parse.js +107 -0
  45. package/dist/model.d.ts +5 -0
  46. package/dist/model.js +4 -1
  47. package/dist/notifications.d.ts +1 -0
  48. package/dist/notifications.js +25 -20
  49. package/dist/participant-host.js +2 -2
  50. package/dist/ports.d.ts +2 -9
  51. package/dist/presentation.d.ts +19 -1
  52. package/dist/presentation.js +111 -73
  53. package/dist/presented.js +2 -1
  54. package/dist/registry.d.ts +8 -4
  55. package/dist/registry.js +43 -76
  56. package/dist/routes.d.ts +19 -2
  57. package/dist/routes.js +48 -5
  58. package/dist/runtime.d.ts +10 -0
  59. package/dist/runtime.js +39 -0
  60. package/dist/square-actions.d.ts +26 -13
  61. package/dist/square-actions.js +203 -87
  62. package/dist/square-application.d.ts +58 -0
  63. package/dist/square-application.js +183 -0
  64. package/dist/square-facade.d.ts +24 -9
  65. package/dist/square-file-adapter.d.ts +5 -12
  66. package/dist/square-file-adapter.js +12 -64
  67. package/dist/square-projections.d.ts +3 -2
  68. package/dist/square-projections.js +12 -3
  69. package/dist/square-storage.js +27 -34
  70. package/dist/square-wiring.d.ts +12 -7
  71. package/dist/square-wiring.js +36 -32
  72. package/dist/state-cell.d.ts +1 -1
  73. package/dist/stream.js +31 -27
  74. package/dist/tty-style.d.ts +7 -0
  75. package/dist/tty-style.js +13 -0
  76. package/dist/views.d.ts +3 -3
  77. package/dist/views.js +10 -9
  78. package/dist/wake-attempts.js +2 -1
  79. package/dist/wake-evidence.js +2 -8
  80. package/dist/wakes.d.ts +1 -0
  81. package/dist/wakes.js +5 -5
  82. package/dist/watch.d.ts +6 -1
  83. package/dist/watch.js +43 -28
  84. package/extensions/square-pi.js +107 -109
  85. package/guides/application-boundary.md +32 -0
  86. package/guides/change-waits.md +28 -0
  87. package/guides/host-ledger.md +9 -0
  88. package/guides/mcp.md +9 -0
  89. package/guides/operation-cancellation.md +17 -0
  90. package/package.json +10 -4
  91. package/skills/square/SKILL.md +5 -7
@@ -1,5 +1,4 @@
1
1
  import type { WakeRouteKind } from './model.js';
2
- export type HostLedgerScope = 'user' | 'local';
3
2
  export type PresenceChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
4
3
  export interface PresenceRecord {
5
4
  readonly location: string;
@@ -11,13 +10,13 @@ export interface PresenceRecord {
11
10
  readonly address: Readonly<Record<string, string>>;
12
11
  };
13
12
  readonly updatedAt?: number;
13
+ readonly epoch?: number;
14
14
  }
15
15
  export type PresenceKey = Pick<PresenceRecord, 'location' | 'participant' | 'session' | 'channel'>;
16
16
  export interface PresenceLookup {
17
17
  readonly location?: string;
18
18
  readonly participant?: string;
19
19
  readonly session?: string;
20
- readonly scopes?: readonly HostLedgerScope[];
21
20
  readonly now?: number;
22
21
  }
23
22
  export type PresenceResult = {
@@ -147,29 +146,11 @@ export type ClaimResult = {
147
146
  readonly status: 'degraded';
148
147
  readonly error: unknown;
149
148
  };
150
- export interface ReconcileBindingInput {
151
- readonly scopes?: readonly HostLedgerScope[];
152
- readonly now?: number;
153
- readonly artifact?: {
154
- read(): Promise<{
155
- state: {
156
- acts: readonly {
157
- kind: string;
158
- actor?: string;
159
- }[];
160
- };
161
- }>;
162
- };
163
- }
164
- export type ReconcileBindingResult = {
165
- readonly status: 'reconciled' | 'degraded';
166
- readonly bindings: readonly PresenceRecord[];
167
- readonly error?: unknown;
168
- };
169
149
  export interface HostLedgerPort {
170
- claimPresence(input: PresenceRecord, scope?: HostLedgerScope): Promise<PresenceClaimResult>;
171
- ensurePresence(input: PresenceRecord, scope?: HostLedgerScope): Promise<PresenceResult>;
150
+ claimPresence(input: PresenceRecord, signal?: AbortSignal): Promise<PresenceClaimResult>;
151
+ ensurePresence(input: PresenceRecord): Promise<PresenceResult>;
172
152
  removePresence(input: PresenceKey): Promise<void>;
153
+ removePresenceIfUnchanged(input: PresenceRecord): Promise<boolean>;
173
154
  listPresence(input: PresenceLookup): Promise<readonly PresenceRecord[]>;
174
155
  claimEvidence(input: EvidenceClaim): Promise<ClaimResult>;
175
156
  releaseEvidence(input: EvidenceRelease): Promise<void>;
@@ -185,5 +166,4 @@ export interface HostLedgerPort {
185
166
  transitionWakeDispatch(input: WakeDispatchTransitionInput): Promise<boolean>;
186
167
  releaseWakeDispatch(input: WakeDispatchReleaseInput): Promise<void>;
187
168
  gcEvidence(input: EvidenceGc): Promise<void>;
188
- reconcileBinding(input: ReconcileBindingInput): Promise<ReconcileBindingResult>;
189
169
  }
package/dist/inbox.d.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import { type InboxMembership } from './model.js';
2
- export declare function hostLedgerForEnv(env: NodeJS.ProcessEnv): import("./host-ledger.js").HostLedgerPort;
2
+ export declare function hostLedgerForEnv(env: NodeJS.ProcessEnv): import("./host-ledger-file-adapter.js").FileHostLedgerPort;
3
3
  export interface PendingWaitOptions {
4
4
  signal?: AbortSignal;
5
- /** Ephemeral watcher de-duplication; never persisted or used by delivery derivation. */
5
+ /** Local delivery suppression, never persisted as evidence of receipt. */
6
6
  excludeKeys?: ReadonlySet<string>;
7
- /** After a delivery failure, wait for a new state edge before retrying the same pending work. */
7
+ /** Arm before dispatch; retry only after a substantive change, not our failed evidence write. */
8
8
  skipImmediate?: boolean;
9
- /** Reports whether a change wait has been established for a deferred retry. */
10
9
  onChangeArmed?: (armed: boolean) => void;
11
10
  }
12
11
  export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<InboxMembership[]>;
13
- /** Wait for a bound square to produce a new pending notification without consuming it. */
12
+ export interface SessionPendingObserver {
13
+ wait(timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
14
+ close(): void;
15
+ }
16
+ /** Session-scoped projection over process-shared storage change detectors. */
17
+ export declare function observeSessionPending(sessionId: string, suppliedEnv?: NodeJS.ProcessEnv): Promise<SessionPendingObserver>;
18
+ /** Finite one-shot consumers retain their API; Pi owns a long-lived observer instead. */
14
19
  export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
package/dist/inbox.js CHANGED
@@ -1,94 +1,179 @@
1
- import { openSquare } from './square-file-adapter.js';
2
- import { closeOpenSquare } from './open-square.js';
3
- import { waitForSquareChanges } from './square-file-adapter.js';
4
- import path from 'node:path';
1
+ import { hostLedgerRoot } from './host-ledger-root.js';
2
+ import { openSquareArtifact, observeSquareChanges } from './square-file-adapter.js';
5
3
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
6
- import { projectPresentation, projectSessionBindings } from './square-projections.js';
4
+ import { canonicalFilePath } from './file-changes.js';
5
+ import { pendingAtBoundary, presentationSuppressesWake, projectPresentation, projectPresentationEvidence, projectSessionBindings } from './square-projections.js';
6
+ import { formatActivityId } from './square-core.js';
7
+ import { WATCH_STALE_MS } from './runtime.js';
7
8
  export function hostLedgerForEnv(env) {
8
- const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
9
- return createHostLedgerPort({
10
- userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
11
- localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
12
- });
9
+ return createHostLedgerPort({ rootPath: hostLedgerRoot(env) });
13
10
  }
14
11
  function notificationKey(membership, actIndex) {
15
- return `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${actIndex}`;
16
- }
17
- function withoutExcluded(inbox, excludeKeys) {
18
- if (excludeKeys === undefined || excludeKeys.size === 0)
19
- return inbox;
20
- return inbox
21
- .map((membership) => ({
22
- ...membership,
23
- notifications: membership.notifications.filter((notification) => !excludeKeys.has(notificationKey(membership, notification.actIndex))),
24
- }))
25
- .filter((membership) => membership.notifications.length > 0);
12
+ return membership.squarePath + '\0' + membership.name.toLocaleLowerCase() + '\0' + actIndex;
26
13
  }
27
14
  export async function sessionInbox(sessionId, env = process.env, signal) {
28
15
  const inbox = [];
29
16
  const hostLedger = hostLedgerForEnv(env);
30
17
  for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
31
- let square;
18
+ if (signal?.aborted)
19
+ throw signal.reason;
20
+ const artifact = openSquareArtifact(binding.location, signal);
32
21
  try {
33
- square = await openSquare(binding.location, { env, hostLedger, signal });
34
- const projection = await projectPresentation({ artifact: square.artifact, binding, now: square.clock() });
22
+ const projection = await projectPresentation({ artifact, binding, now: Date.now() });
35
23
  if (!projection.joined)
36
24
  continue;
37
- inbox.push({
38
- name: projection.binding.participant,
39
- squarePath: projection.binding.location,
25
+ inbox.push({ name: projection.binding.participant, squarePath: projection.binding.location,
40
26
  notifications: [...projection.notifications],
41
- ...(projection.catchLease !== undefined ? { catchLease: projection.catchLease } : {}),
42
- });
27
+ ...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) });
43
28
  }
44
- catch {
45
- // A stale discovery-cache row only disables delivery for that membership.
29
+ catch (error) {
30
+ if (signal?.aborted)
31
+ throw signal.reason ?? error;
32
+ // A stale or unreadable membership cannot disable delivery for other memberships.
46
33
  }
47
34
  finally {
48
- if (square !== undefined)
49
- await closeOpenSquare(square);
35
+ await artifact.close();
50
36
  }
51
37
  }
52
38
  return inbox;
53
39
  }
54
- /** Wait for a bound square to produce a new pending notification without consuming it. */
55
- export async function waitForSessionPending(sessionId, timeoutMs, options = {}, env = process.env) {
56
- const deadline = Date.now() + Math.max(0, timeoutMs);
57
- if (!options.skipImmediate) {
58
- const immediate = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
59
- if (immediate.some((membership) => membership.notifications.length > 0))
60
- return immediate;
40
+ async function capture(observer, kind, signal) {
41
+ const generation = observer.generation;
42
+ try {
43
+ const value = await observer.read(signal);
44
+ return { kind, wait: (timeout, abort) => observer.changed(value, timeout, abort) };
61
45
  }
62
- if (timeoutMs <= 0 || options.signal?.aborted) {
63
- options.onChangeArmed?.(false);
64
- return [];
46
+ catch (error) {
47
+ if (signal.aborted)
48
+ throw signal.reason ?? error;
49
+ // Keep watching an unreadable/missing artifact so repair can restore delivery.
50
+ return { kind, wait: (timeout, abort) => observer.hinted(generation, timeout, abort) };
65
51
  }
66
- const bindings = await projectSessionBindings({ hostLedger: hostLedgerForEnv(env), sessionId });
67
- const paths = [...new Set(bindings.map((binding) => binding.location))];
68
- let aborted = false;
69
- let projectAfterReady = !options.skipImmediate;
70
- const onAbort = () => { aborted = true; };
71
- options.signal?.addEventListener('abort', onAbort, { once: true });
72
- try {
73
- while (!aborted) {
74
- const remaining = deadline - Date.now();
75
- if (remaining <= 0)
76
- return [];
77
- const change = await waitForSquareChanges(paths, remaining, options.signal, async () => {
78
- if (!projectAfterReady)
79
- return undefined;
80
- const current = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
81
- return current.some((membership) => membership.notifications.length > 0) ? current : undefined;
82
- }, options.onChangeArmed);
83
- if (aborted || change.status === 'expired')
52
+ }
53
+ /** Session-scoped projection over process-shared storage change detectors. */
54
+ export async function observeSessionPending(sessionId, suppliedEnv = process.env) {
55
+ const env = { ...suppliedEnv };
56
+ const hostLedger = hostLedgerForEnv(env);
57
+ const ledger = await hostLedger.observeChanges();
58
+ const artifacts = new Map();
59
+ const lifetime = new AbortController();
60
+ return {
61
+ async wait(timeoutMs, options = {}) {
62
+ const deadline = Date.now() + Math.max(0, timeoutMs);
63
+ const expired = new AbortController();
64
+ const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
65
+ ? setTimeout(() => expired.abort(), Math.min(timeoutMs, 2_147_483_647)) : undefined;
66
+ const signal = AbortSignal.any([lifetime.signal, expired.signal, ...(options.signal === undefined ? [] : [options.signal])]);
67
+ let canReturn = !options.skipImmediate;
68
+ let previousEligible;
69
+ let armed = false;
70
+ try {
71
+ while (!signal.aborted) {
72
+ // Subscribe/capture the ledger BEFORE discovering bindings; a concurrent join cannot fall in a gap.
73
+ const sources = await Promise.all([capture(ledger.presence, 'presence', signal), capture(ledger.evidence, 'evidence', signal)]);
74
+ const bindings = await projectSessionBindings({ hostLedger, sessionId });
75
+ const paths = new Set(await Promise.all(bindings.map((binding) => canonicalFilePath(binding.location))));
76
+ for (const [file, observer] of artifacts) {
77
+ if (!paths.has(file)) {
78
+ observer.close();
79
+ artifacts.delete(file);
80
+ }
81
+ }
82
+ for (const file of paths) {
83
+ if (artifacts.has(file))
84
+ continue;
85
+ const observer = await observeSquareChanges(file);
86
+ if (signal.aborted) {
87
+ observer.close();
88
+ return [];
89
+ }
90
+ artifacts.set(file, observer);
91
+ }
92
+ sources.push(...await Promise.all([...artifacts.values()].map((observer) => capture(observer, 'artifact', signal))));
93
+ const inbox = await sessionInbox(sessionId, env, signal);
94
+ const pending = [];
95
+ for (const membership of pendingAtBoundary(inbox)) {
96
+ const evidence = await projectPresentationEvidence({ hostLedger, location: membership.squarePath, participant: membership.name, sessionId });
97
+ const notifications = membership.notifications.filter((notification) => !options.excludeKeys?.has(notificationKey(membership, notification.actIndex)) &&
98
+ !presentationSuppressesWake(evidence.filter((row) => row.activity === formatActivityId(notification.actIndex))));
99
+ if (notifications.length > 0)
100
+ pending.push({ ...membership, notifications });
101
+ }
102
+ const eligible = JSON.stringify(pending.flatMap((membership) => membership.notifications.map((note) => notificationKey(membership, note.actIndex))).sort());
103
+ if (previousEligible !== undefined && previousEligible !== eligible)
104
+ canReturn = true;
105
+ previousEligible = eligible;
106
+ if (signal.aborted)
107
+ return [];
108
+ if (canReturn && pending.length > 0)
109
+ return pending;
110
+ const remaining = deadline - Date.now();
111
+ if (remaining <= 0)
112
+ return [];
113
+ // Eligibility may change at lease expiry without any storage write.
114
+ const leaseExpiry = Math.min(Infinity, ...inbox.flatMap((membership) => membership.catchLease === undefined ? [] : [Math.min(membership.catchLease.expiresAt, membership.catchLease.heartbeatAt + WATCH_STALE_MS + 1)]));
115
+ const nextExpiry = Math.min(leaseExpiry, await hostLedger.nextExpiry(sessionId));
116
+ const duration = Math.min(remaining, Math.max(1, nextExpiry - Date.now()));
117
+ const cancelled = new AbortController();
118
+ const waitSignal = AbortSignal.any([signal, cancelled.signal]);
119
+ const waits = sources.map(async (source) => {
120
+ try {
121
+ return await source.wait(duration, waitSignal) ? source.kind : 'deadline';
122
+ }
123
+ catch (error) {
124
+ if (waitSignal.aborted)
125
+ throw error;
126
+ return source.kind;
127
+ }
128
+ });
129
+ if (!armed) {
130
+ armed = true;
131
+ options.onChangeArmed?.(true);
132
+ }
133
+ try {
134
+ const change = await Promise.race(waits);
135
+ // Evidence writes from our own failed injection must not generate a retry loop.
136
+ if (change !== 'evidence')
137
+ canReturn = true;
138
+ }
139
+ finally {
140
+ cancelled.abort();
141
+ await Promise.allSettled(waits);
142
+ }
143
+ }
84
144
  return [];
85
- if (change.status === 'ready')
86
- return change.value;
87
- projectAfterReady = true;
88
- }
89
- return [];
145
+ }
146
+ catch (error) {
147
+ if (signal.aborted)
148
+ return [];
149
+ throw error;
150
+ }
151
+ finally {
152
+ if (timer !== undefined)
153
+ clearTimeout(timer);
154
+ if (!armed)
155
+ options.onChangeArmed?.(false);
156
+ }
157
+ },
158
+ close() {
159
+ if (lifetime.signal.aborted)
160
+ return;
161
+ lifetime.abort();
162
+ ledger.presence.close();
163
+ ledger.evidence.close();
164
+ for (const observer of artifacts.values())
165
+ observer.close();
166
+ artifacts.clear();
167
+ },
168
+ };
169
+ }
170
+ /** Finite one-shot consumers retain their API; Pi owns a long-lived observer instead. */
171
+ export async function waitForSessionPending(sessionId, timeoutMs, options = {}, env = process.env) {
172
+ const observer = await observeSessionPending(sessionId, env);
173
+ try {
174
+ return await observer.wait(timeoutMs, options);
90
175
  }
91
176
  finally {
92
- options.signal?.removeEventListener('abort', onAbort);
177
+ observer.close();
93
178
  }
94
179
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { Square } from './square-wiring.js';
2
+ export { createSquareApplication } from './square-application.js';
2
3
  export { SquareError } from './model.js';
3
4
  export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
4
5
  export { createDefaultWakeTransport } from './notifications.js';
5
6
  export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
6
7
  export type { HostLedgerPort, PresenceRecord, EvidenceRecord } from './host-ledger.js';
7
8
  export type { PresentationSinkPort } from './ports.js';
9
+ export type { SquareApplication, SquareApplicationContext, JoinApplicationOptions, JoinApplicationResult, ExpressApplicationOptions, ApplicationStatus, ApplicationStatusProjection, ApplicationParticipants } from './square-application.js';
8
10
  export type { ActivityId } from './square-core.js';
9
- export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource, } from './square-facade.js';
11
+ export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, OperationControl, OperationProgress, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource, } from './square-facade.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Square } from './square-wiring.js';
2
+ export { createSquareApplication } from './square-application.js';
2
3
  export { SquareError } from './model.js';
3
4
  export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
4
5
  export { createDefaultWakeTransport } from './notifications.js';
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) {
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type SquareApplicationContext } from './square-application.js';
3
+ export interface SquareMcpOptions extends SquareApplicationContext {
4
+ }
5
+ export type SquareMcpServer = McpServer;
6
+ export declare function createSquareMcpServer(options: SquareMcpOptions): SquareMcpServer;
7
+ export declare function runSquareMcpStdio(options: SquareMcpOptions): Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,122 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod/v4';
4
+ import { SquareError, isSquareError } from './model.js';
5
+ import { createSquareApplication } from './square-application.js';
6
+ const activityIdSchema = { type: 'string', pattern: '^act/(0|[1-9][0-9]*)$' };
7
+ const stringArraySchema = { type: 'array', items: { type: 'string' } };
8
+ function objectSchema(properties, required = []) { return { properties, required }; }
9
+ const TOOL_DEFINITIONS = [
10
+ { name: 'join', description: 'Join the square as the configured participant.', inputSchema: objectSchema({ takeover: { type: 'boolean' } }) },
11
+ { name: 'express', description: 'Express a message or embodied action in the square.', inputSchema: objectSchema({ body: { type: 'string' }, mentions: stringArraySchema, reach: { type: 'string', enum: ['bell'] }, reply: activityIdSchema, force: { type: 'boolean' }, noWait: { type: 'boolean' } }, ['body']) },
12
+ { name: 'catch', description: 'Catch and consume activities perceptible to the configured participant.', inputSchema: objectSchema({ id: activityIdSchema, idle: { type: 'number', minimum: 0 }, from: stringArraySchema, mention: { type: 'boolean' }, limit: { type: 'integer', minimum: 1 } }) },
13
+ { name: 'history', description: 'Read the activity stream using stable activity-id cursors.', inputSchema: objectSchema({ limit: { type: 'integer', minimum: 1 }, order: { type: 'string', enum: ['asc', 'desc'] }, before: activityIdSchema, after: activityIdSchema, grep: { type: 'string' }, fixed: { type: 'string' }, from: stringArraySchema, mention: { type: 'string' } }) },
14
+ { name: 'listen', description: 'Listen for future activity from a participant.', inputSchema: objectSchema({ target: { type: 'string' } }, ['target']) },
15
+ { name: 'ignore', description: 'Stop listening for future activity from a participant.', inputSchema: objectSchema({ target: { type: 'string' } }, ['target']) },
16
+ { name: 'listening', description: 'List participants this participant is listening to.', inputSchema: objectSchema({}) },
17
+ { name: 'hold', description: 'Raise a hand to hold the square.', inputSchema: objectSchema({ reason: { type: 'string' } }) },
18
+ { name: 'resume', description: 'Release the current hold.', inputSchema: objectSchema({}) },
19
+ { name: 'done', description: 'Leave the square without adding a message.', inputSchema: objectSchema({}) },
20
+ { name: 'status', description: 'Read the public status of the square.', inputSchema: objectSchema({}) },
21
+ { name: 'participants', description: 'List participants standing in or done with the square.', inputSchema: objectSchema({}) },
22
+ ];
23
+ function zodInputSchema(schema) {
24
+ const shape = {};
25
+ for (const [key, property] of Object.entries(schema.properties)) {
26
+ let field;
27
+ if (property.type === 'string' && property.enum !== undefined)
28
+ field = z.enum(property.enum);
29
+ else if (property.type === 'string')
30
+ field = property.pattern === undefined ? z.string() : z.string().regex(new RegExp(property.pattern));
31
+ else if (property.type === 'boolean')
32
+ field = z.boolean();
33
+ else if (property.type === 'number')
34
+ field = z.number().finite().min(property.minimum ?? -Infinity);
35
+ else if (property.type === 'integer')
36
+ field = z.number().int().min(property.minimum ?? -Infinity);
37
+ else if (property.type === 'array')
38
+ field = z.array(z.string());
39
+ else
40
+ throw new Error(`Unsupported input schema type: ${property.type}`);
41
+ shape[key] = schema.required.includes(key) ? field : field.optional();
42
+ }
43
+ return z.object(shape).strict();
44
+ }
45
+ function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); }
46
+ function publicActivity(activity) { return { ...activity }; }
47
+ function publicResult(name, value) {
48
+ if (name === 'status') {
49
+ const result = value;
50
+ const state = result.status;
51
+ const { latestAct, participants, ...rest } = state;
52
+ return {
53
+ hardCap: rest.hardCap,
54
+ ...(rest.throttlePerMinute === undefined ? {} : { throttlePerMinute: rest.throttlePerMinute }),
55
+ activeCount: rest.activeCount,
56
+ doneCount: rest.doneCount,
57
+ hold: rest.holdActive ? { by: rest.holdActor, ...(rest.holdReason === undefined ? {} : { reason: rest.holdReason }), ...(rest.holdAt === undefined ? {} : { at: rest.holdAt }) } : null,
58
+ participants: participants.filter((participant) => participant.state !== 'not joined').map((participant) => ({ name: participant.name, state: participant.state, presence: participant.presence, activityCount: participant.activityCount, ...(participant.lastActiveAt === undefined ? {} : { lastActiveAt: participant.lastActiveAt }), unreadActivityCount: participant.unreadActivityCount, pendingMentionCount: participant.pendingMentionCount, listening: participant.listening })),
59
+ ...(latestAct === undefined ? {} : { latestActivity: publicActivity({ id: `act/${latestAct.index}`, at: latestAct.at, kind: latestAct.kind, actor: latestAct.actor ?? '', ...('body' in latestAct && latestAct.body !== undefined ? { body: latestAct.body } : {}), mentions: 'mentions' in latestAct ? latestAct.mentions ?? [] : [], ...('target' in latestAct ? { target: latestAct.target } : {}), ...(latestAct.kind === 'say' && latestAct.reach !== undefined ? { reach: latestAct.reach } : {}), ...(latestAct.kind === 'say' && latestAct.reply !== undefined ? { reply: `act/${latestAct.reply}` } : {}) }) }),
60
+ now: rest.now,
61
+ };
62
+ }
63
+ if (name === 'express' || name === 'hold' || name === 'resume' || name === 'done') {
64
+ const result = value;
65
+ return { ...(result.activity === undefined ? {} : { activity: result.activity === null ? null : publicActivity(result.activity) }), ...(result.waited === undefined ? {} : { waited: result.waited }) };
66
+ }
67
+ if (name === 'history')
68
+ return { activities: value };
69
+ if (name === 'participants')
70
+ return { participants: value };
71
+ if (name === 'listening')
72
+ return { listening: value };
73
+ return value;
74
+ }
75
+ function operation(app, name, args, control) {
76
+ switch (name) {
77
+ case 'join': return app.join({ takeover: args.takeover }, control);
78
+ case 'express': {
79
+ const { body, noWait, ...options } = args;
80
+ return app.express(body, { ...options, noWait }, control);
81
+ }
82
+ case 'catch': return app.catch(args, control);
83
+ case 'history': return app.history(args, control);
84
+ case 'listen': return app.listen(args.target, control);
85
+ case 'ignore': return app.ignore(args.target, control);
86
+ case 'listening': return app.listening(control);
87
+ case 'hold': return app.hold(args.reason, control);
88
+ case 'resume': return app.resume(control);
89
+ case 'done': return app.done(undefined, control);
90
+ case 'status': return app.status();
91
+ case 'participants': return app.participants();
92
+ default: throw new SquareError('invalid_args', `Unknown tool: ${name}`);
93
+ }
94
+ }
95
+ export function createSquareMcpServer(options) {
96
+ const app = createSquareApplication(options);
97
+ const server = new McpServer({ name: 'square', version: '0.3.61' }, {
98
+ instructions: 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.',
99
+ });
100
+ for (const tool of TOOL_DEFINITIONS) {
101
+ server.registerTool(tool.name, { description: tool.description, inputSchema: zodInputSchema(tool.inputSchema) }, async (args, extra) => {
102
+ try {
103
+ const value = await operation(app, tool.name, args, { signal: extra.signal });
104
+ const projected = publicResult(tool.name, value);
105
+ const structuredContent = isObject(projected) ? projected : { result: projected };
106
+ return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent };
107
+ }
108
+ catch (error) {
109
+ const normalized = extra.signal.aborted
110
+ ? { code: 'cancelled', message: 'The MCP request was cancelled' }
111
+ : isSquareError(error)
112
+ ? { code: error.code, message: error.message, ...(error.facts === undefined ? {} : { facts: error.facts }) }
113
+ : { code: 'internal', message: error instanceof Error ? error.message : String(error) };
114
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(normalized) }], structuredContent: { error: normalized } };
115
+ }
116
+ });
117
+ }
118
+ return server;
119
+ }
120
+ export async function runSquareMcpStdio(options) {
121
+ await createSquareMcpServer(options).connect(new StdioServerTransport());
122
+ }
@@ -0,0 +1,8 @@
1
+ export interface MentionCandidate {
2
+ /** The maximal name-grammar token after `@` (roster-free; resolution may shorten it). */
3
+ readonly token: string;
4
+ /** UTF-16 index of the `@` in the body. */
5
+ readonly index: number;
6
+ }
7
+ export declare function scanMentionCandidates(body: string): MentionCandidate[];
8
+ export declare function resolveNameAt(body: string, candidate: MentionCandidate, names: readonly string[]): string | undefined;