@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
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { cmdActivity } from '../activity.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { nowMs } from '../runtime.js';
|
|
2
|
+
import { scanMentionCandidates } from '../mention-parse.js';
|
|
3
|
+
import { validateDoneBody } from '../decisions.js';
|
|
4
|
+
import { formatHardCap, } from '../model.js';
|
|
5
|
+
import { formatRefusal, participantCommandPrefix, participantIdentity, takeoverRecoveryLines, renderRoomChangeText, renderAmbientEvent, withPathOutput, } from '../presentation.js';
|
|
6
|
+
import { hostLedgerForEnv, hasAutomaticDeliveryIdentity, } from '../registry.js';
|
|
7
|
+
import { actId, nowMs } from '../runtime.js';
|
|
8
8
|
import { createSquare, openSquare } from '../square-file-adapter.js';
|
|
9
9
|
import { closeOpenSquare } from '../open-square.js';
|
|
10
|
-
import {
|
|
10
|
+
import { Square } from '../square-wiring.js';
|
|
11
|
+
import { createSquareApplication } from '../square-application.js';
|
|
11
12
|
import { entryPresentation, eventPresentation } from '../views.js';
|
|
12
13
|
import { createDefaultWakeTransport } from '../notifications.js';
|
|
14
|
+
import { style } from '../tty-style.js';
|
|
13
15
|
import { fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
|
|
14
16
|
const JOIN_DEFAULT_LAST = 10;
|
|
15
17
|
const JOIN_MAX_LAST = 100;
|
|
@@ -57,7 +59,7 @@ export const buildCommand = {
|
|
|
57
59
|
await createSquare(squarePath, intent.options, snippet);
|
|
58
60
|
const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
|
|
59
61
|
const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
|
|
60
|
-
return withPathOutput(squarePath, ['✓
|
|
62
|
+
return withPathOutput(squarePath, ['✓ the square is open', ` · cap ${cap}`, ...throttle, ' · nobody here yet — the first join steps in'].join('\n'), { participantCount: 0 });
|
|
61
63
|
},
|
|
62
64
|
present: (result) => process.stdout.write(result),
|
|
63
65
|
};
|
|
@@ -84,77 +86,60 @@ export const joinCommand = {
|
|
|
84
86
|
parse: parseJoin,
|
|
85
87
|
async execute(intent, context) {
|
|
86
88
|
const squarePath = requireSquarePath(context);
|
|
87
|
-
const
|
|
89
|
+
const hostLedger = hostLedgerForEnv(context.env);
|
|
90
|
+
const beforeSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger });
|
|
88
91
|
const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
|
|
89
92
|
await closeOpenSquare(beforeSquare);
|
|
90
|
-
const
|
|
91
|
-
|
|
93
|
+
const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
|
|
94
|
+
let joinedName;
|
|
95
|
+
let joinKind;
|
|
92
96
|
try {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const result = await application.join({ takeover: intent.kick });
|
|
98
|
+
joinedName = result.participant;
|
|
99
|
+
joinKind = result.kind;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error instanceof Error && 'code' in error && error.code === 'already_joined') {
|
|
103
|
+
process.stderr.write(formatRefusal(squarePath, [
|
|
99
104
|
`✕ ${participantIdentity(intent.name)} shoos you out of the square`,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
const takeoverNeeded = isRejoin && intent.kick;
|
|
106
|
-
const oldBindings = takeoverNeeded ? await lookupParticipant(squarePath, intent.name) : [];
|
|
107
|
-
let participant;
|
|
108
|
-
if (takeoverNeeded) {
|
|
109
|
-
const currentSessions = new Set(sessionIdsFromEnvironment());
|
|
110
|
-
await square.takeover(intent.name);
|
|
111
|
-
for (const binding of oldBindings) {
|
|
112
|
-
if (!currentSessions.has(binding.sessionId)) {
|
|
113
|
-
await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel).catch(() => undefined);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
await claimSessionParticipant(squarePath, intent.name, process.env);
|
|
117
|
-
participant = await square.join(intent.name);
|
|
105
|
+
' · a same-named participant stands here — the name is taken',
|
|
106
|
+
...takeoverRecoveryLines(squarePath, intent.name),
|
|
107
|
+
], { participantCount: before.participantCount }));
|
|
108
|
+
process.exit(2);
|
|
118
109
|
}
|
|
119
|
-
|
|
120
|
-
await claimSessionParticipant(squarePath, intent.name, process.env);
|
|
121
|
-
participant = await square.join(intent.name);
|
|
122
|
-
}
|
|
123
|
-
const joinedName = participant.name;
|
|
124
|
-
const afterSquare = await openSquare(squarePath, { clock: nowMs });
|
|
125
|
-
const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
|
|
126
|
-
await closeOpenSquare(afterSquare);
|
|
127
|
-
await square.reconcileBinding().catch(() => undefined);
|
|
128
|
-
const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
|
|
129
|
-
now: nowMs(),
|
|
130
|
-
preview: intent.lastN === null ? undefined : 200,
|
|
131
|
-
actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
|
|
132
|
-
squareState: after.state,
|
|
133
|
-
})).filter(Boolean).join('\n\n');
|
|
134
|
-
const contextText = after.joinContext;
|
|
135
|
-
const fallback = hasAutomaticDeliveryIdentity()
|
|
136
|
-
? []
|
|
137
|
-
: ['', `» ${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' no session delivery detected — keep this catch open for new activity'];
|
|
138
|
-
const scene = after.scene;
|
|
139
|
-
const entryLine = !isRejoin
|
|
140
|
-
? '● You stepped into the square'
|
|
141
|
-
: reconnect && !intent.kick
|
|
142
|
-
? '● you are already in the square'
|
|
143
|
-
: `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
|
|
144
|
-
const output = [
|
|
145
|
-
entryLine,
|
|
146
|
-
'',
|
|
147
|
-
"Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.",
|
|
148
|
-
...(reconnect || scene === '' ? [] : ['', scene]),
|
|
149
|
-
...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
|
|
150
|
-
...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
|
|
151
|
-
...fallback,
|
|
152
|
-
].join('\n');
|
|
153
|
-
return withPathOutput(squarePath, output, { participantCount: after.participantCount });
|
|
154
|
-
}
|
|
155
|
-
finally {
|
|
156
|
-
await square.close();
|
|
110
|
+
throw error;
|
|
157
111
|
}
|
|
112
|
+
const afterSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger });
|
|
113
|
+
const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
|
|
114
|
+
await closeOpenSquare(afterSquare);
|
|
115
|
+
const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
|
|
116
|
+
now: nowMs(),
|
|
117
|
+
preview: intent.lastN === null ? undefined : 200,
|
|
118
|
+
actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
|
|
119
|
+
squareState: after.state,
|
|
120
|
+
})).filter(Boolean).join('\n\n');
|
|
121
|
+
const contextText = after.joinContext;
|
|
122
|
+
const fallback = hasAutomaticDeliveryIdentity()
|
|
123
|
+
? []
|
|
124
|
+
: ['', `${participantCommandPrefix(squarePath, joinedName)} catch --idle 30m`, ' the square has no way to call you — keep this catch open and stay within earshot'];
|
|
125
|
+
const scene = after.scene;
|
|
126
|
+
const entryLine = joinKind === 'joined'
|
|
127
|
+
? '● you stepped into the square'
|
|
128
|
+
: joinKind === 'reconnected'
|
|
129
|
+
? '● you are already in the square'
|
|
130
|
+
: `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
|
|
131
|
+
const isRejoin = joinKind !== 'joined';
|
|
132
|
+
const reconnect = joinKind === 'reconnected';
|
|
133
|
+
const output = [
|
|
134
|
+
entryLine,
|
|
135
|
+
'',
|
|
136
|
+
"· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
|
|
137
|
+
...(reconnect || scene === '' ? [] : ['', scene]),
|
|
138
|
+
...(isRejoin || contextText === '' ? [] : ['', style('dim', 'context'), contextText]),
|
|
139
|
+
...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
|
|
140
|
+
...fallback,
|
|
141
|
+
].join('\n');
|
|
142
|
+
return withPathOutput(squarePath, output, { participantCount: after.participantCount });
|
|
158
143
|
},
|
|
159
144
|
present: (result) => process.stdout.write(result),
|
|
160
145
|
};
|
|
@@ -173,20 +158,17 @@ function parseActivity(argv, context) {
|
|
|
173
158
|
else if (argument === '--no-wait')
|
|
174
159
|
noWait = true;
|
|
175
160
|
else if (argument === '--beside')
|
|
176
|
-
fail('✕ express does not know --beside\
|
|
161
|
+
fail('✕ express does not know --beside\nsquare express --help');
|
|
177
162
|
else if (argument === '--bell')
|
|
178
163
|
bell = true;
|
|
179
164
|
else if (argument === '--no-mention')
|
|
180
165
|
noMention = true;
|
|
181
166
|
else if (argument === '--mention') {
|
|
182
|
-
mentions.push(
|
|
167
|
+
mentions.push(requireValue(argv, index, argument));
|
|
183
168
|
index += 1;
|
|
184
169
|
}
|
|
185
170
|
else if (argument === '--reply') {
|
|
186
|
-
|
|
187
|
-
if (replyIndex === undefined)
|
|
188
|
-
fail('Invalid --reply: expected an activity id like act/12');
|
|
189
|
-
reply = replyIndex;
|
|
171
|
+
reply = requireValue(argv, index, argument);
|
|
190
172
|
index += 1;
|
|
191
173
|
}
|
|
192
174
|
else
|
|
@@ -194,11 +176,9 @@ function parseActivity(argv, context) {
|
|
|
194
176
|
}
|
|
195
177
|
const reach = bell ? 'bell' : undefined;
|
|
196
178
|
if (bell && (noMention || mentions.length > 0))
|
|
197
|
-
fail('✕ --bell cannot be combined with --mention or --no-mention\
|
|
179
|
+
fail('✕ --bell cannot be combined with --mention or --no-mention\nsquare express --help');
|
|
198
180
|
if (noMention && mentions.length > 0)
|
|
199
|
-
fail('✕ --no-mention cannot be combined with --mention\
|
|
200
|
-
if (!bell && !noMention && mentions.length === 0)
|
|
201
|
-
fail('✕ express needs --mention <name>, --no-mention, or --bell\n» square express --help');
|
|
181
|
+
fail('✕ --no-mention cannot be combined with --mention\nsquare express --help');
|
|
202
182
|
if (bodyArgs.length !== 1) {
|
|
203
183
|
if (bodyArgs.length === 0) {
|
|
204
184
|
if (!process.stdin.isTTY)
|
|
@@ -213,14 +193,18 @@ export const expressCommand = {
|
|
|
213
193
|
async execute(intent, context) {
|
|
214
194
|
const squarePath = requireSquarePath(context);
|
|
215
195
|
const body = await resolveBody(intent.activity);
|
|
216
|
-
|
|
196
|
+
if (intent.reach === undefined && !intent.noMention && intent.mentions.length === 0 && scanMentionCandidates(body).length === 0) {
|
|
197
|
+
fail('✕ express needs --mention <name>, --no-mention, --bell, or an @name in the body\nsquare express --help');
|
|
198
|
+
}
|
|
217
199
|
await cmdActivity(squarePath, intent.name, body, (value) => value, {
|
|
218
200
|
force: intent.force,
|
|
219
201
|
noWait: intent.noWait,
|
|
202
|
+
noMention: intent.noMention,
|
|
220
203
|
mentions: intent.mentions,
|
|
221
204
|
reach: intent.reach,
|
|
222
205
|
reply: intent.reply,
|
|
223
|
-
|
|
206
|
+
cwd: context.cwd,
|
|
207
|
+
env: context.env,
|
|
224
208
|
});
|
|
225
209
|
},
|
|
226
210
|
present: () => { },
|
|
@@ -247,15 +231,12 @@ export const listenCommand = {
|
|
|
247
231
|
async execute(intent, context) {
|
|
248
232
|
const squarePath = requireSquarePath(context);
|
|
249
233
|
const square = await Square.at({ path: squarePath, clock: nowMs });
|
|
250
|
-
const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
|
|
251
234
|
try {
|
|
252
|
-
const
|
|
253
|
-
const result = await participant.listen(intent.target);
|
|
235
|
+
const result = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs }).listen(intent.target);
|
|
254
236
|
const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
|
|
255
|
-
return listenerPresentation(squarePath,
|
|
237
|
+
return listenerPresentation(squarePath, intent.name, intent.target, 'listen', result.activity, participantCount);
|
|
256
238
|
}
|
|
257
239
|
finally {
|
|
258
|
-
await facade.close();
|
|
259
240
|
await square.close();
|
|
260
241
|
}
|
|
261
242
|
},
|
|
@@ -266,15 +247,12 @@ export const ignoreCommand = {
|
|
|
266
247
|
async execute(intent, context) {
|
|
267
248
|
const squarePath = requireSquarePath(context);
|
|
268
249
|
const square = await Square.at({ path: squarePath, clock: nowMs });
|
|
269
|
-
const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
|
|
270
250
|
try {
|
|
271
|
-
const
|
|
272
|
-
const result = await participant.ignore(intent.target);
|
|
251
|
+
const result = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs }).ignore(intent.target);
|
|
273
252
|
const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
|
|
274
|
-
return listenerPresentation(squarePath,
|
|
253
|
+
return listenerPresentation(squarePath, intent.name, intent.target, 'ignore', result.activity, participantCount);
|
|
275
254
|
}
|
|
276
255
|
finally {
|
|
277
|
-
await facade.close();
|
|
278
256
|
await square.close();
|
|
279
257
|
}
|
|
280
258
|
},
|
|
@@ -285,10 +263,9 @@ export const listeningCommand = {
|
|
|
285
263
|
async execute(intent, context) {
|
|
286
264
|
const squarePath = requireSquarePath(context);
|
|
287
265
|
const square = await Square.at({ path: squarePath, clock: nowMs });
|
|
288
|
-
const facade = await openParticipant({ path: squarePath, clock: nowMs }, intent.name);
|
|
289
266
|
try {
|
|
290
|
-
const participant =
|
|
291
|
-
const targets = await participant.listening();
|
|
267
|
+
const participant = { name: intent.name };
|
|
268
|
+
const targets = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, participant: intent.name, clock: nowMs }).listening();
|
|
292
269
|
const participantCount = (await square.snapshot()).participants.filter((item) => item.state === 'joined').length;
|
|
293
270
|
const body = targets.length === 0
|
|
294
271
|
? `○ ${participantIdentity(participant.name)} is not turned toward anyone`
|
|
@@ -296,31 +273,38 @@ export const listeningCommand = {
|
|
|
296
273
|
return withPathOutput(squarePath, body, { participantCount });
|
|
297
274
|
}
|
|
298
275
|
finally {
|
|
299
|
-
await facade.close();
|
|
300
276
|
await square.close();
|
|
301
277
|
}
|
|
302
278
|
},
|
|
303
279
|
present: (result) => process.stdout.write(result),
|
|
304
280
|
};
|
|
281
|
+
function validateDoneInput(body) {
|
|
282
|
+
try {
|
|
283
|
+
validateDoneBody(body);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
fail(`${error instanceof Error ? error.message : String(error)}\nsquare done --help`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
305
289
|
function parseDone(argv, context) {
|
|
306
290
|
if (argv.length > 1)
|
|
307
291
|
usage(context.command);
|
|
292
|
+
if (argv[0] !== '-')
|
|
293
|
+
validateDoneInput(argv[0]);
|
|
308
294
|
return { name: requireParticipant(context.name), body: argv.length === 1 ? argv[0] : undefined };
|
|
309
295
|
}
|
|
310
296
|
export const doneCommand = {
|
|
311
297
|
parse: parseDone,
|
|
312
298
|
async execute(intent, context) {
|
|
313
299
|
const squarePath = requireSquarePath(context);
|
|
314
|
-
const body =
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const result = await
|
|
318
|
-
await square.reconcileBinding();
|
|
319
|
-
await square.close();
|
|
300
|
+
const body = intent.body === '-' || !process.stdin.isTTY ? await readStdin() : '';
|
|
301
|
+
validateDoneInput(body);
|
|
302
|
+
const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs, participant: intent.name });
|
|
303
|
+
const result = await application.done(body);
|
|
320
304
|
const name = result.activity.actor;
|
|
321
|
-
const presentation = await openSquare(squarePath, { clock: nowMs });
|
|
305
|
+
const presentation = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
|
|
322
306
|
const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
|
|
323
|
-
return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
|
|
307
|
+
return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · ${result.activity.id} · just now`, { participantCount });
|
|
324
308
|
},
|
|
325
309
|
present: (result) => process.stdout.write(result),
|
|
326
310
|
};
|
|
@@ -333,19 +317,12 @@ export const holdCommand = {
|
|
|
333
317
|
parse: parseHold,
|
|
334
318
|
async execute(intent, context) {
|
|
335
319
|
const squarePath = requireSquarePath(context);
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
343
|
-
await closeOpenSquare(presentationSquare);
|
|
344
|
-
return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount, held: true });
|
|
345
|
-
}
|
|
346
|
-
finally {
|
|
347
|
-
await square.close();
|
|
348
|
-
}
|
|
320
|
+
const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs, participant: intent.name });
|
|
321
|
+
const result = await application.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
|
|
322
|
+
const presentationSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
|
|
323
|
+
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
324
|
+
await closeOpenSquare(presentationSquare);
|
|
325
|
+
return withPathOutput(squarePath, `· ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount, held: true });
|
|
349
326
|
},
|
|
350
327
|
present: (result) => process.stdout.write(result),
|
|
351
328
|
};
|
|
@@ -357,19 +334,12 @@ export const resumeCommand = {
|
|
|
357
334
|
},
|
|
358
335
|
async execute(intent, context) {
|
|
359
336
|
const squarePath = requireSquarePath(context);
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
367
|
-
await closeOpenSquare(presentationSquare);
|
|
368
|
-
return withPathOutput(squarePath, renderEventCli(presentation.activity), { participantCount: presentation.participantCount });
|
|
369
|
-
}
|
|
370
|
-
finally {
|
|
371
|
-
await square.close();
|
|
372
|
-
}
|
|
337
|
+
const application = createSquareApplication({ cwd: context.cwd, env: context.env, squarePath, clock: nowMs, participant: intent.name });
|
|
338
|
+
const result = await application.resume();
|
|
339
|
+
const presentationSquare = await openSquare(squarePath, { clock: nowMs, env: context.env, hostLedger: hostLedgerForEnv(context.env) });
|
|
340
|
+
const presentation = await eventPresentation(presentationSquare, result.activity.id);
|
|
341
|
+
await closeOpenSquare(presentationSquare);
|
|
342
|
+
return withPathOutput(squarePath, `✓ ${renderRoomChangeText(presentation.activity)} · ${actId(presentation.activity.index)} · just now`, { participantCount: presentation.participantCount });
|
|
373
343
|
},
|
|
374
344
|
present: (result) => process.stdout.write(result),
|
|
375
345
|
};
|
package/dist/decisions.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export type ActDecision = {
|
|
|
58
58
|
type: 'bell_quota';
|
|
59
59
|
nextAt: number;
|
|
60
60
|
};
|
|
61
|
+
/** The unread activity a viewer has not caught up to, grouped by speaker. */
|
|
62
|
+
export declare function unreadActivitySummaries(squareState: SquareState, name: string, now: number): UnreadActivitySummary[];
|
|
61
63
|
export declare function decideAct(squareState: SquareState, input: {
|
|
62
64
|
name: string;
|
|
63
65
|
body: string;
|
|
@@ -66,7 +68,9 @@ export declare function decideAct(squareState: SquareState, input: {
|
|
|
66
68
|
mentions?: readonly string[];
|
|
67
69
|
reach?: Reach;
|
|
68
70
|
reply?: number;
|
|
71
|
+
noMention?: boolean;
|
|
69
72
|
}): ActDecision;
|
|
73
|
+
export declare function validateDoneBody(body: unknown): void;
|
|
70
74
|
export declare function coreDone(squareState: SquareState, name: string, body: string, now: number): Extract<Act, {
|
|
71
75
|
kind: 'done';
|
|
72
76
|
}>;
|
package/dist/decisions.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { SquareError, sameName, validateName, } from './model.js';
|
|
2
2
|
import { participantIdentity } from './participant-identity.js';
|
|
3
|
-
import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, THROTTLE_WINDOW_MS, } from './runtime.js';
|
|
3
|
+
import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
|
|
4
4
|
import { actDelta, directedPeerSays } from './activity-feed.js';
|
|
5
5
|
import { formatActivityId, isIgnored, isListening, listeningTo, MAX_IDENTITY_SET_SIZE, validate } from './square-core.js';
|
|
6
6
|
import { deriveDeliveryModel } from './delivery.js';
|
|
7
7
|
import { compileSearchPattern } from './search.js';
|
|
8
|
+
import { scanMentionCandidates, resolveNameAt } from './mention-parse.js';
|
|
8
9
|
export function resolveKnownName(squareState, name) {
|
|
9
10
|
validateName(name);
|
|
10
11
|
const known = resolveRosterName(squareState, name);
|
|
11
12
|
if (known === undefined) {
|
|
12
|
-
throw new SquareError('invalid_args',
|
|
13
|
+
throw new SquareError('invalid_args', `✕ ${participantIdentity(name)} has never stepped into this square`);
|
|
13
14
|
}
|
|
14
15
|
return known;
|
|
15
16
|
}
|
|
@@ -66,6 +67,40 @@ function requireStanding(squareState, act) {
|
|
|
66
67
|
throw new Error(`Unexpected standing validation result: ${result.reason}`);
|
|
67
68
|
}
|
|
68
69
|
const UNREAD_PREVIEW_LIMIT = 3;
|
|
70
|
+
/** The unread activity a viewer has not caught up to, grouped by speaker. */
|
|
71
|
+
export function unreadActivitySummaries(squareState, name, now) {
|
|
72
|
+
const delivery = deriveDeliveryModel(squareState);
|
|
73
|
+
const delta = actDelta(squareState.acts, delivery.cursorFor(name));
|
|
74
|
+
const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
|
|
75
|
+
const sayCountByActor = new Map();
|
|
76
|
+
const unreadByParticipant = new Map();
|
|
77
|
+
for (const item of unreadPublic) {
|
|
78
|
+
if (item.kind === 'say') {
|
|
79
|
+
const key = item.actor.toLocaleLowerCase();
|
|
80
|
+
sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
|
|
81
|
+
}
|
|
82
|
+
if (sameName(item.actor, name))
|
|
83
|
+
continue;
|
|
84
|
+
const actorKey = item.actor.toLocaleLowerCase();
|
|
85
|
+
const currentSummary = unreadByParticipant.get(item.actor);
|
|
86
|
+
unreadByParticipant.set(item.actor, {
|
|
87
|
+
count: (currentSummary?.count ?? 0) + 1,
|
|
88
|
+
latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
|
|
89
|
+
previews: [
|
|
90
|
+
...(currentSummary?.previews ?? []),
|
|
91
|
+
{ number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
|
|
92
|
+
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return [...unreadByParticipant.entries()]
|
|
96
|
+
.map(([participant, summary]) => ({
|
|
97
|
+
name: participant,
|
|
98
|
+
count: summary.count,
|
|
99
|
+
latestActivityAgeMs: Math.max(0, now - summary.latestAt),
|
|
100
|
+
previews: summary.previews,
|
|
101
|
+
}))
|
|
102
|
+
.sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
|
|
103
|
+
}
|
|
69
104
|
export function decideAct(squareState, input) {
|
|
70
105
|
const { now, force } = input;
|
|
71
106
|
const name = resolveStandingName(squareState, input.name);
|
|
@@ -81,7 +116,7 @@ export function decideAct(squareState, input) {
|
|
|
81
116
|
validateName(requested);
|
|
82
117
|
const resolved = joinedNames.find((candidate) => sameName(candidate, requested));
|
|
83
118
|
if (resolved === undefined) {
|
|
84
|
-
throw new SquareError('invalid_args',
|
|
119
|
+
throw new SquareError('invalid_args', `✕ ${participantIdentity(requested)} is not standing in this square`);
|
|
85
120
|
}
|
|
86
121
|
if (!mentionNames.some((existing) => sameName(existing, resolved))) {
|
|
87
122
|
if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
|
|
@@ -90,8 +125,42 @@ export function decideAct(squareState, input) {
|
|
|
90
125
|
mentionNames.push(resolved);
|
|
91
126
|
}
|
|
92
127
|
}
|
|
128
|
+
if (input.noMention !== true) {
|
|
129
|
+
// Body @name tokens resolve against the standing roster at landing time; the stored
|
|
130
|
+
// body never changes, only mention metadata is derived. Explicit --no-mention turns
|
|
131
|
+
// this scan off entirely.
|
|
132
|
+
const unmatched = [];
|
|
133
|
+
for (const candidate of scanMentionCandidates(body)) {
|
|
134
|
+
const resolved = resolveNameAt(body, candidate, joinedNames);
|
|
135
|
+
if (resolved === undefined) {
|
|
136
|
+
unmatched.push(candidate);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!mentionNames.some((existing) => sameName(existing, resolved))) {
|
|
140
|
+
if (mentionNames.length >= MAX_IDENTITY_SET_SIZE) {
|
|
141
|
+
throw new SquareError('invalid_args', `An activity can mention at most ${MAX_IDENTITY_SET_SIZE} participants`);
|
|
142
|
+
}
|
|
143
|
+
mentionNames.push(resolved);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// A bell already reaches everyone: only there does an unmatched @token stay literal text.
|
|
147
|
+
if (reach !== 'bell' && unmatched.length > 0) {
|
|
148
|
+
const roster = rosterNames(squareState);
|
|
149
|
+
const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate, roster) !== undefined);
|
|
150
|
+
const unknown = unmatched.filter((candidate) => !notStanding.includes(candidate));
|
|
151
|
+
const lines = [];
|
|
152
|
+
if (unknown.length > 0) {
|
|
153
|
+
lines.push(`✕ ${unknown.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${unknown.length === 1 ? 'does' : 'do'} not match anyone in this square`);
|
|
154
|
+
}
|
|
155
|
+
if (notStanding.length > 0) {
|
|
156
|
+
lines.push(`✕ ${notStanding.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${notStanding.length === 1 ? 'is' : 'are'} not standing in this square`);
|
|
157
|
+
}
|
|
158
|
+
lines.push(' · wrap literal @text in backticks, or pass --no-mention to land bare');
|
|
159
|
+
throw new SquareError('invalid_args', lines.join('\n'));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
93
162
|
if (reach === 'bell' && mentionNames.length > 0) {
|
|
94
|
-
throw new SquareError('invalid_args', 'A bell cannot be combined with --mention.');
|
|
163
|
+
throw new SquareError('invalid_args', 'A bell cannot be combined with a mention — remove --mention or the @name in the body.');
|
|
95
164
|
}
|
|
96
165
|
const reply = input.reply;
|
|
97
166
|
if (reply !== undefined) {
|
|
@@ -126,34 +195,7 @@ export function decideAct(squareState, input) {
|
|
|
126
195
|
const delta = actDelta(squareState.acts, delivery.cursorFor(name));
|
|
127
196
|
const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
|
|
128
197
|
const unreadRoomChanges = [];
|
|
129
|
-
const
|
|
130
|
-
const unreadByParticipant = new Map();
|
|
131
|
-
for (const item of unreadPublic) {
|
|
132
|
-
if (item.kind === 'say') {
|
|
133
|
-
const key = item.actor.toLocaleLowerCase();
|
|
134
|
-
sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
|
|
135
|
-
}
|
|
136
|
-
if (sameName(item.actor, name))
|
|
137
|
-
continue;
|
|
138
|
-
const actorKey = item.actor.toLocaleLowerCase();
|
|
139
|
-
const currentSummary = unreadByParticipant.get(item.actor);
|
|
140
|
-
unreadByParticipant.set(item.actor, {
|
|
141
|
-
count: (currentSummary?.count ?? 0) + 1,
|
|
142
|
-
latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
|
|
143
|
-
previews: [
|
|
144
|
-
...(currentSummary?.previews ?? []),
|
|
145
|
-
{ number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
|
|
146
|
-
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const activitySummaries = [...unreadByParticipant.entries()]
|
|
150
|
-
.map(([participant, summary]) => ({
|
|
151
|
-
name: participant,
|
|
152
|
-
count: summary.count,
|
|
153
|
-
latestActivityAgeMs: Math.max(0, now - summary.latestAt),
|
|
154
|
-
previews: summary.previews,
|
|
155
|
-
}))
|
|
156
|
-
.sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
|
|
198
|
+
const activitySummaries = unreadActivitySummaries(squareState, name, now);
|
|
157
199
|
const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
|
|
158
200
|
const hasUnread = unreadPublic.length > 0;
|
|
159
201
|
const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
|
|
@@ -168,15 +210,21 @@ export function decideAct(squareState, input) {
|
|
|
168
210
|
...(reach !== undefined ? { reach } : {}),
|
|
169
211
|
...(reply !== undefined ? { reply } : {}),
|
|
170
212
|
},
|
|
171
|
-
confirmation: `●
|
|
213
|
+
confirmation: `● your activity lands — #${ownActCount}`,
|
|
172
214
|
ownActCount,
|
|
173
215
|
pendingPublic: unreadPublic,
|
|
174
216
|
pendingRoomChanges: unreadRoomChanges,
|
|
175
217
|
};
|
|
176
218
|
}
|
|
219
|
+
export function validateDoneBody(body) {
|
|
220
|
+
if (body !== undefined && body !== '') {
|
|
221
|
+
throw new SquareError('invalid_args', 'done only leaves the square and does not accept a message; use express before leaving');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
177
224
|
export function coreDone(squareState, name, body, now) {
|
|
225
|
+
validateDoneBody(body);
|
|
178
226
|
const resolvedName = resolveStandingName(squareState, name);
|
|
179
|
-
const act = { kind: 'done', actor: resolvedName, at: now
|
|
227
|
+
const act = { kind: 'done', actor: resolvedName, at: now };
|
|
180
228
|
requireStanding(squareState, act);
|
|
181
229
|
return act;
|
|
182
230
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { type SquareState } from './model.js';
|
|
2
|
-
import type { HostLedgerPort, PresenceRecord, PresentationEvidenceProjection, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput,
|
|
2
|
+
import type { HostLedgerPort, PresenceRecord, PresentationEvidenceProjection, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, SquareObservation } from './ports.js';
|
|
3
3
|
import { deriveDeliveryModel } from './delivery.js';
|
|
4
4
|
import { type WakeAttempt } from './square-projections.js';
|
|
5
5
|
export declare function observeSquare(input: ObserveSquareInput): Promise<SquareObservation>;
|
|
6
|
-
export declare function reconcileBinding(input: ReconcileBindingInput): Promise<import("./host-ledger.js").ReconcileBindingResult>;
|
|
7
6
|
export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
|
|
8
7
|
export declare function selectPendingWakeActivities(state: SquareState, routes: readonly PresenceRecord[], attempts: readonly WakeAttempt[], now: number, graceMs: number, limit: number, delivery?: import("./delivery.js").DeliveryModel, presentations?: readonly PresentationEvidenceProjection[]): number[];
|
|
9
8
|
export declare function sweepPending(input: {
|
|
@@ -39,7 +39,7 @@ export async function observeSquare(input) {
|
|
|
39
39
|
let rows = [];
|
|
40
40
|
if (input.hostLedger !== undefined) {
|
|
41
41
|
try {
|
|
42
|
-
rows = await input.hostLedger.listPresence({ location: input.location,
|
|
42
|
+
rows = await input.hostLedger.listPresence({ location: input.location, now: input.now });
|
|
43
43
|
}
|
|
44
44
|
catch {
|
|
45
45
|
rows = [];
|
|
@@ -55,9 +55,6 @@ export async function observeSquare(input) {
|
|
|
55
55
|
}));
|
|
56
56
|
return { ...(input.location === undefined ? {} : { location: input.location }), version: snapshot.version, state: snapshot.state, pending: delivery.joinedRecipients().map((recipient) => ({ recipient, notifications: delivery.pendingFor(recipient) })), bindings };
|
|
57
57
|
}
|
|
58
|
-
export async function reconcileBinding(input) {
|
|
59
|
-
return input.hostLedger.reconcileBinding({ artifact: input.artifact, scopes: input.scopes, now: input.now });
|
|
60
|
-
}
|
|
61
58
|
export async function deliverPending(input) {
|
|
62
59
|
const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
|
|
63
60
|
const routes = (observation.state.routes ?? []).map((route) => ({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function canonicalFilePath(value: string): Promise<string>;
|
|
2
|
+
export interface VersionObserver<T> {
|
|
3
|
+
readonly generation: number;
|
|
4
|
+
hinted(since: number, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
|
|
5
|
+
read(signal?: AbortSignal): Promise<T>;
|
|
6
|
+
changed(since: T, timeoutMs: number, signal?: AbortSignal): Promise<boolean>;
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
/** Shared only within this process. Durable state is always read at the storage boundary. */
|
|
10
|
+
export declare function observeFileVersion<T>(filePath: string, kind: string, read: (file: string, signal: AbortSignal) => Promise<T>): Promise<VersionObserver<T>>;
|
|
11
|
+
/** File metadata invalidates a ledger projection; it is not presentation evidence. */
|
|
12
|
+
export declare function observeFileMetadata(file: string): Promise<VersionObserver<string>>;
|