@astrosheep/square 0.3.61 → 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 (87) 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 +168 -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 +8 -6
  10. package/dist/automatic-session.js +14 -24
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/context.js +2 -18
  15. package/dist/cli/harness-command.js +1 -1
  16. package/dist/cli/mcp-server-command.d.ts +2 -0
  17. package/dist/cli/mcp-server-command.js +13 -0
  18. package/dist/cli/observation-commands.js +47 -19
  19. package/dist/cli/program.js +5 -2
  20. package/dist/cli/registry.js +2 -0
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +93 -125
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +44 -3
  25. package/dist/delivery-operations.d.ts +1 -2
  26. package/dist/delivery-operations.js +1 -4
  27. package/dist/file-changes.d.ts +12 -0
  28. package/dist/file-changes.js +235 -0
  29. package/dist/help.js +4 -8
  30. package/dist/host-ledger-file-adapter.d.ts +15 -13
  31. package/dist/host-ledger-file-adapter.js +131 -79
  32. package/dist/host-ledger-root.d.ts +2 -0
  33. package/dist/host-ledger-root.js +9 -0
  34. package/dist/host-ledger.d.ts +4 -24
  35. package/dist/inbox.d.ts +10 -5
  36. package/dist/inbox.js +152 -67
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +1 -0
  39. package/dist/mcp.d.ts +7 -0
  40. package/dist/mcp.js +122 -0
  41. package/dist/mention-parse.d.ts +8 -0
  42. package/dist/mention-parse.js +107 -0
  43. package/dist/model.d.ts +5 -0
  44. package/dist/model.js +4 -1
  45. package/dist/notifications.d.ts +1 -0
  46. package/dist/notifications.js +25 -20
  47. package/dist/participant-host.js +2 -2
  48. package/dist/ports.d.ts +2 -9
  49. package/dist/presentation.d.ts +12 -1
  50. package/dist/presentation.js +42 -12
  51. package/dist/presented.js +2 -1
  52. package/dist/registry.d.ts +8 -4
  53. package/dist/registry.js +41 -74
  54. package/dist/routes.d.ts +19 -2
  55. package/dist/routes.js +48 -5
  56. package/dist/runtime.d.ts +2 -0
  57. package/dist/runtime.js +2 -0
  58. package/dist/square-actions.d.ts +26 -13
  59. package/dist/square-actions.js +200 -84
  60. package/dist/square-application.d.ts +58 -0
  61. package/dist/square-application.js +183 -0
  62. package/dist/square-facade.d.ts +24 -9
  63. package/dist/square-file-adapter.d.ts +5 -12
  64. package/dist/square-file-adapter.js +12 -64
  65. package/dist/square-projections.d.ts +3 -2
  66. package/dist/square-projections.js +12 -3
  67. package/dist/square-storage.js +27 -34
  68. package/dist/square-wiring.d.ts +12 -7
  69. package/dist/square-wiring.js +36 -32
  70. package/dist/state-cell.d.ts +1 -1
  71. package/dist/stream.js +30 -26
  72. package/dist/views.d.ts +3 -3
  73. package/dist/views.js +4 -4
  74. package/dist/wake-attempts.js +2 -1
  75. package/dist/wake-evidence.js +2 -8
  76. package/dist/wakes.d.ts +1 -0
  77. package/dist/wakes.js +5 -5
  78. package/dist/watch.d.ts +6 -1
  79. package/dist/watch.js +20 -18
  80. package/extensions/square-pi.js +100 -112
  81. package/guides/application-boundary.md +32 -0
  82. package/guides/change-waits.md +28 -0
  83. package/guides/host-ledger.md +9 -0
  84. package/guides/mcp.md +9 -0
  85. package/guides/operation-cancellation.md +17 -0
  86. package/package.json +8 -2
  87. 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/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;
@@ -0,0 +1,107 @@
1
+ import { NAME_GRAPHEME_SOURCE, sameName } from './model.js';
2
+ /**
3
+ * Roster-free scan for `@name` candidates in an activity body. Resolution against the
4
+ * standing roster happens at landing time in decideAct; this scanner only knows the
5
+ * name grammar and Markdown code spans.
6
+ *
7
+ * Guard rails:
8
+ * - Backtick code spans never yield candidates (also the escape hatch: `` `@name` `` stays literal).
9
+ * - A `@` glued to a handle-like prefix does not start a candidate, so emails
10
+ * (`user@host`), URLs (`x.com/@user`), and `snake_@case` stay literal. Only ASCII
11
+ * word characters, `_`, `-`, `/`, and `@` block a boundary: CJK adjacency like
12
+ * `告诉@rei` stays mentionable because CJK text has no spaces to lean on.
13
+ * - The token must be a complete name (slash-separated segments allowed); resolution
14
+ * never backs off to a shorter prefix, so `@rei2` does not mention `rei`.
15
+ */
16
+ const MENTION_TOKEN = new RegExp(`@((?:${NAME_GRAPHEME_SOURCE})+(?:/(?:${NAME_GRAPHEME_SOURCE})+)*)`, 'vg');
17
+ const BOUNDARY_BLOCKER = /[A-Za-z0-9_\-/@]/;
18
+ /** Ranges [from, to) covered by Markdown code spans (backtick runs close at a run of equal length). */
19
+ function codeSpanRanges(body) {
20
+ const ranges = [];
21
+ let index = 0;
22
+ while (index < body.length) {
23
+ if (body[index] !== '`') {
24
+ index += 1;
25
+ continue;
26
+ }
27
+ let runEnd = index;
28
+ while (runEnd < body.length && body[runEnd] === '`')
29
+ runEnd += 1;
30
+ const runLength = runEnd - index;
31
+ let scan = runEnd;
32
+ let closedEnd = -1;
33
+ while (scan < body.length) {
34
+ if (body[scan] !== '`') {
35
+ scan += 1;
36
+ continue;
37
+ }
38
+ let closeEnd = scan;
39
+ while (closeEnd < body.length && body[closeEnd] === '`')
40
+ closeEnd += 1;
41
+ if (closeEnd - scan === runLength) {
42
+ closedEnd = closeEnd;
43
+ break;
44
+ }
45
+ scan = closeEnd;
46
+ }
47
+ if (closedEnd === -1) {
48
+ // Unmatched backticks are literal text; scanning continues after the run.
49
+ index = runEnd;
50
+ continue;
51
+ }
52
+ ranges.push([index, closedEnd]);
53
+ index = closedEnd;
54
+ }
55
+ return ranges;
56
+ }
57
+ function codePointBefore(body, index) {
58
+ if (index <= 0)
59
+ return undefined;
60
+ const unit = body.charCodeAt(index - 1);
61
+ if (unit >= 0xdc00 && unit <= 0xdfff && index >= 2)
62
+ return String.fromCodePoint(body.codePointAt(index - 2));
63
+ return String.fromCodePoint(unit);
64
+ }
65
+ export function scanMentionCandidates(body) {
66
+ const spans = codeSpanRanges(body);
67
+ const found = [];
68
+ const seen = new Set();
69
+ for (const match of body.matchAll(MENTION_TOKEN)) {
70
+ const start = match.index;
71
+ if (spans.some(([from, to]) => start >= from && start < to))
72
+ continue;
73
+ const previous = codePointBefore(body, start);
74
+ if (previous !== undefined && BOUNDARY_BLOCKER.test(previous))
75
+ continue;
76
+ const token = match[1];
77
+ const key = token.toLocaleLowerCase();
78
+ if (!seen.has(key)) {
79
+ seen.add(key);
80
+ found.push({ token, index: start });
81
+ }
82
+ }
83
+ return found;
84
+ }
85
+ /**
86
+ * Resolve the longest standing name starting at one candidate's position. A partial
87
+ * match is only accepted when the character right after it is not handle-continuation
88
+ * (ASCII word char, `_`, `-`, `/`): `@rei2` never degrades into mentioning `rei`, while
89
+ * CJK-fluent text like `告诉@rei这个` still mentions `rei`.
90
+ */
91
+ const TRAILING_BLOCKER = /[A-Za-z0-9_\-/]/;
92
+ export function resolveNameAt(body, candidate, names) {
93
+ const text = body.slice(candidate.index + 1, candidate.index + 1 + candidate.token.length);
94
+ let best;
95
+ for (const name of names) {
96
+ if (name.length > text.length)
97
+ continue;
98
+ if (!sameName(text.slice(0, name.length), name))
99
+ continue;
100
+ const after = text[name.length];
101
+ if (after !== undefined && TRAILING_BLOCKER.test(after))
102
+ continue;
103
+ if (best === undefined || name.length > best.length)
104
+ best = name;
105
+ }
106
+ return best;
107
+ }
package/dist/model.d.ts CHANGED
@@ -18,8 +18,11 @@ export interface ReceiverRoute extends WakeRoute {
18
18
  export type SquareErrorCode = 'invalid_name' | 'invalid_args' | 'unknown_participant' | 'not_joined' | 'already_joined' | 'already_done' | 'held' | 'capped' | 'throttled' | 'bell_quota' | 'behind' | 'io' | 'unavailable';
19
19
  export type InternalSquareErrorCode = SquareErrorCode | 'not_found' | 'cap_reached' | 'conflict' | 'pending_peer';
20
20
  export interface SquareErrorFacts {
21
+ /** Express admission rejected before any activity was committed. */
22
+ activityUnsent?: true;
21
23
  pending?: number;
22
24
  holder?: string;
25
+ holdReason?: string;
23
26
  retryAfterMs?: number;
24
27
  }
25
28
  declare class SquareErrorBase<Code extends string> extends Error {
@@ -132,4 +135,6 @@ export declare function parseParticipantList(value: string): string[];
132
135
  export declare function nameKey(name: string): string;
133
136
  export declare function sameName(a: string, b: string): boolean;
134
137
  export declare function findParticipantName(participants: string[], name: string): string | undefined;
138
+ /** One name grapheme: Unicode letters, digits, marks, hyphens, or underscores (no isolated variation selectors), or a complete RGI emoji. Shared by name validation and body @mention scanning. */
139
+ export declare const NAME_GRAPHEME_SOURCE: string;
135
140
  export declare function validateName(name: string): void;
package/dist/model.js CHANGED
@@ -39,8 +39,11 @@ export function sameName(a, b) {
39
39
  export function findParticipantName(participants, name) {
40
40
  return participants.find((participant) => sameName(participant, name));
41
41
  }
42
+ /** One name grapheme: Unicode letters, digits, marks, hyphens, or underscores (no isolated variation selectors), or a complete RGI emoji. Shared by name validation and body @mention scanning. */
43
+ export const NAME_GRAPHEME_SOURCE = String.raw `[[\p{L}\p{N}\p{M}_\x2D]--[\p{Variation_Selector}]]|\p{RGI_Emoji}`;
44
+ const NAME_PATTERN = new RegExp(`^(?:${NAME_GRAPHEME_SOURCE})+(?:/(?:${NAME_GRAPHEME_SOURCE})+)*$`, 'v');
42
45
  export function validateName(name) {
43
- if (!name || !/^(?:[[\p{L}\p{N}\p{M}_\x2D]--[\p{Variation_Selector}]]|\p{RGI_Emoji})+(?:\/(?:[[\p{L}\p{N}\p{M}_\x2D]--[\p{Variation_Selector}]]|\p{RGI_Emoji})+)*$/v.test(name)) {
46
+ if (!name || !NAME_PATTERN.test(name)) {
44
47
  throw new SquareError('invalid_name', 'Invalid name: names must contain non-empty slash-separated segments using Unicode letters, digits, marks, hyphens, underscores, or complete RGI emoji graphemes.');
45
48
  }
46
49
  }