@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.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/skills/square/SKILL.md +5 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.d.ts +6 -2
- package/dist/activity.js +170 -133
- package/dist/artifact.d.ts +4 -0
- package/dist/artifact.js +43 -14
- package/dist/attention-presentation.d.ts +1 -0
- package/dist/attention-presentation.js +21 -19
- package/dist/automatic-session.js +16 -26
- package/dist/boundary-presentation.d.ts +1 -2
- package/dist/boundary-presentation.js +4 -19
- package/dist/catch-decisions.js +1 -1
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/context.js +17 -33
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/mcp-server-command.d.ts +2 -0
- package/dist/cli/mcp-server-command.js +13 -0
- package/dist/cli/observation-commands.js +132 -64
- package/dist/cli/program.js +36 -9
- package/dist/cli/registry.js +2 -0
- package/dist/cli/square-commands.d.ts +1 -1
- package/dist/cli/square-commands.js +103 -133
- package/dist/decisions.d.ts +4 -0
- package/dist/decisions.js +82 -34
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +1 -4
- package/dist/file-changes.d.ts +12 -0
- package/dist/file-changes.js +235 -0
- package/dist/help.js +4 -8
- package/dist/host-ledger-file-adapter.d.ts +15 -13
- package/dist/host-ledger-file-adapter.js +131 -79
- package/dist/host-ledger-root.d.ts +2 -0
- package/dist/host-ledger-root.js +9 -0
- package/dist/host-ledger.d.ts +4 -24
- package/dist/inbox.d.ts +10 -5
- package/dist/inbox.js +152 -67
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/list.js +12 -9
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +122 -0
- package/dist/mention-parse.d.ts +8 -0
- package/dist/mention-parse.js +107 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +4 -1
- package/dist/notifications.d.ts +1 -0
- package/dist/notifications.js +25 -20
- package/dist/participant-host.js +2 -2
- package/dist/ports.d.ts +2 -9
- package/dist/presentation.d.ts +19 -1
- package/dist/presentation.js +111 -73
- package/dist/presented.js +2 -1
- package/dist/registry.d.ts +8 -4
- package/dist/registry.js +43 -76
- package/dist/routes.d.ts +19 -2
- package/dist/routes.js +48 -5
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +39 -0
- package/dist/square-actions.d.ts +26 -13
- package/dist/square-actions.js +203 -87
- package/dist/square-application.d.ts +58 -0
- package/dist/square-application.js +183 -0
- package/dist/square-facade.d.ts +24 -9
- package/dist/square-file-adapter.d.ts +5 -12
- package/dist/square-file-adapter.js +12 -64
- package/dist/square-projections.d.ts +3 -2
- package/dist/square-projections.js +12 -3
- package/dist/square-storage.js +27 -34
- package/dist/square-wiring.d.ts +12 -7
- package/dist/square-wiring.js +36 -32
- package/dist/state-cell.d.ts +1 -1
- package/dist/stream.js +31 -27
- package/dist/tty-style.d.ts +7 -0
- package/dist/tty-style.js +13 -0
- package/dist/views.d.ts +3 -3
- package/dist/views.js +10 -9
- package/dist/wake-attempts.js +2 -1
- package/dist/wake-evidence.js +2 -8
- package/dist/wakes.d.ts +1 -0
- package/dist/wakes.js +5 -5
- package/dist/watch.d.ts +6 -1
- package/dist/watch.js +43 -28
- package/extensions/square-pi.js +107 -109
- package/guides/application-boundary.md +32 -0
- package/guides/change-waits.md +28 -0
- package/guides/host-ledger.md +9 -0
- package/guides/mcp.md +9 -0
- package/guides/operation-cancellation.md +17 -0
- package/package.json +10 -4
- package/skills/square/SKILL.md +5 -7
package/dist/host-ledger.d.ts
CHANGED
|
@@ -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,
|
|
171
|
-
ensurePresence(input: PresenceRecord
|
|
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").
|
|
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
|
-
/**
|
|
5
|
+
/** Local delivery suppression, never persisted as evidence of receipt. */
|
|
6
6
|
excludeKeys?: ReadonlySet<string>;
|
|
7
|
-
/**
|
|
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
|
-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
+
if (signal?.aborted)
|
|
19
|
+
throw signal.reason;
|
|
20
|
+
const artifact = openSquareArtifact(binding.location, signal);
|
|
32
21
|
try {
|
|
33
|
-
|
|
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
|
|
42
|
-
});
|
|
27
|
+
...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) });
|
|
43
28
|
}
|
|
44
|
-
catch {
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
await closeOpenSquare(square);
|
|
35
|
+
await artifact.close();
|
|
50
36
|
}
|
|
51
37
|
}
|
|
52
38
|
return inbox;
|
|
53
39
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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 ? [`○
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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(
|
|
115
|
+
lines.push(`square list --depth ${depth} --limit ${limit} --after ${quoteShell(cursor)}`);
|
|
113
116
|
}
|
|
114
117
|
if (stopped)
|
|
115
|
-
lines.push(`○
|
|
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;
|