@astrosheep/square 0.3.2
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/codex-plugin/.codex-plugin/plugin.json +25 -0
- package/codex-plugin/hooks/hooks.json +28 -0
- package/dist/activity-feed.js +36 -0
- package/dist/activity.js +151 -0
- package/dist/artifact.js +739 -0
- package/dist/claude-hook.js +112 -0
- package/dist/cmd/notify-once.js +37 -0
- package/dist/compact.js +39 -0
- package/dist/decisions.js +286 -0
- package/dist/delivery-health.js +249 -0
- package/dist/delivery.js +93 -0
- package/dist/doctor.js +34 -0
- package/dist/harness.js +584 -0
- package/dist/help.js +131 -0
- package/dist/inbox.js +33 -0
- package/dist/index.js +163 -0
- package/dist/list.js +126 -0
- package/dist/model.js +44 -0
- package/dist/notifications.js +97 -0
- package/dist/paseo-timeline.js +206 -0
- package/dist/presentation.js +468 -0
- package/dist/presented.js +211 -0
- package/dist/registry.js +299 -0
- package/dist/runtime.js +304 -0
- package/dist/search.js +54 -0
- package/dist/square-core.js +183 -0
- package/dist/square.js +1366 -0
- package/dist/stream.js +149 -0
- package/dist/terminal.js +125 -0
- package/dist/time.js +81 -0
- package/dist/wake-sink.js +219 -0
- package/dist/watch.js +386 -0
- package/extensions/square-opencode.js +87 -0
- package/extensions/square-pi.js +167 -0
- package/guides/architect.md +165 -0
- package/guides/brainstorm.md +404 -0
- package/guides/participant.md +171 -0
- package/package.json +57 -0
- package/skills/brainstorm/SKILL.md +136 -0
- package/skills/square/.claude-plugin/plugin.json +8 -0
- package/skills/square/SKILL.md +154 -0
- package/skills/square/hooks/hooks.json +27 -0
- package/skills/square-feedback/SKILL.md +55 -0
- package/skills/square-feedback/agents/openai.yaml +4 -0
- package/template.md +4 -0
- package/templates/architect.md +4 -0
- package/templates/brainstorm.md +4 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { leaseOwnsNotification } from './delivery.js';
|
|
2
|
+
import { notificationMessageId } from './delivery-health.js';
|
|
3
|
+
import { sessionInbox } from './inbox.js';
|
|
4
|
+
import { participantCommandPrefix } from './presentation.js';
|
|
5
|
+
import { presentOnce } from './presented.js';
|
|
6
|
+
function pendingCount(inbox) {
|
|
7
|
+
return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
|
|
8
|
+
}
|
|
9
|
+
const INJECT_BODY_MAX = 2048;
|
|
10
|
+
/** Let a fresh blocking catch own notifications it can deliver; hook injection remains the fallback. */
|
|
11
|
+
export function deferToActiveCatch(inbox) {
|
|
12
|
+
return inbox
|
|
13
|
+
.map((membership) => {
|
|
14
|
+
const lease = membership.catchLease;
|
|
15
|
+
if (lease === undefined)
|
|
16
|
+
return membership;
|
|
17
|
+
return {
|
|
18
|
+
...membership,
|
|
19
|
+
notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, notification)),
|
|
20
|
+
};
|
|
21
|
+
})
|
|
22
|
+
.filter((membership) => membership.notifications.length > 0);
|
|
23
|
+
}
|
|
24
|
+
function injectBodyPreview(body, squarePath, name, actIndex) {
|
|
25
|
+
const compact = body.replace(/\r\n/g, '\n');
|
|
26
|
+
if (compact.length <= INJECT_BODY_MAX)
|
|
27
|
+
return compact;
|
|
28
|
+
const pointer = `${participantCommandPrefix(squarePath, name)} echo --ids act_${actIndex} --full`;
|
|
29
|
+
return `${compact.slice(0, INJECT_BODY_MAX).trimEnd()}\n… [truncated] full echo: ${pointer}`;
|
|
30
|
+
}
|
|
31
|
+
export function renderClaudeInboxContext(inbox) {
|
|
32
|
+
const count = pendingCount(inbox);
|
|
33
|
+
const noun = count === 1 ? 'notification' : 'notifications';
|
|
34
|
+
return [
|
|
35
|
+
`<system-reminder source="square">You have ${count} unread Square ${noun}.`,
|
|
36
|
+
...inbox.flatMap((membership) => {
|
|
37
|
+
const command = `${participantCommandPrefix(membership.squarePath, membership.name)} catch --now`;
|
|
38
|
+
return membership.notifications.map((notification) => {
|
|
39
|
+
const id = notificationMessageId(membership.squarePath, notification.actIndex);
|
|
40
|
+
const body = injectBodyPreview(notification.body, membership.squarePath, membership.name, notification.actIndex);
|
|
41
|
+
return [
|
|
42
|
+
`${id} · ${membership.squarePath}: @${membership.name} from @${notification.actor} (${notification.via})`,
|
|
43
|
+
body,
|
|
44
|
+
`Ack with: ${command}`,
|
|
45
|
+
].join('\n');
|
|
46
|
+
});
|
|
47
|
+
}),
|
|
48
|
+
// Body here is a cache only. Delivered is written solely by catch.
|
|
49
|
+
'Ids are stable across turns. If you already acted on an id, do not repeat the action; still run catch --now to mark delivered.',
|
|
50
|
+
'Read and respond in the square before finishing the current turn.</system-reminder>',
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
function nativeHookResponse(input, lookup, env) {
|
|
54
|
+
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
55
|
+
return undefined;
|
|
56
|
+
if (input.hook_event_name !== 'UserPromptSubmit' && input.hook_event_name !== 'Stop') {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (input.hook_event_name === 'Stop' && input.stop_hook_active === true)
|
|
60
|
+
return undefined;
|
|
61
|
+
// Delivery membership is only claimed by explicit participant actions (join/act/catch/...).
|
|
62
|
+
// Inherited PASEO_AGENT_ID proves process ancestry, not conversational ownership.
|
|
63
|
+
// Stop is the guaranteed "don't leave while undelivered" nudge; it does not consume presentation.
|
|
64
|
+
if (input.hook_event_name === 'Stop') {
|
|
65
|
+
const pending = lookup(input.session_id).filter((membership) => membership.notifications.length > 0);
|
|
66
|
+
if (pendingCount(pending) === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
return { decision: 'block', reason: renderClaudeInboxContext(pending) };
|
|
69
|
+
}
|
|
70
|
+
return presentOnce(input.session_id, (sessionId) => deferToActiveCatch(lookup(sessionId)), (inbox) => ({
|
|
71
|
+
hookSpecificOutput: {
|
|
72
|
+
hookEventName: 'UserPromptSubmit',
|
|
73
|
+
additionalContext: renderClaudeInboxContext(inbox),
|
|
74
|
+
},
|
|
75
|
+
}), env);
|
|
76
|
+
}
|
|
77
|
+
export function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
78
|
+
return nativeHookResponse(input, lookup, env);
|
|
79
|
+
}
|
|
80
|
+
/** Codex shares Claude's turn-boundary protocol; keep a dedicated command for churn isolation. */
|
|
81
|
+
export function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
82
|
+
return nativeHookResponse(input, lookup, env);
|
|
83
|
+
}
|
|
84
|
+
export function opencodeHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
85
|
+
return nativeHookResponse(input, lookup, env);
|
|
86
|
+
}
|
|
87
|
+
export function runClaudeHook(inputText, env = process.env) {
|
|
88
|
+
let input;
|
|
89
|
+
try {
|
|
90
|
+
input = JSON.parse(inputText);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
if (input === null || typeof input !== 'object')
|
|
96
|
+
return '';
|
|
97
|
+
const response = claudeHookResponse(input, sessionInbox, env);
|
|
98
|
+
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
99
|
+
}
|
|
100
|
+
export function runCodexHook(inputText, env = process.env) {
|
|
101
|
+
let input;
|
|
102
|
+
try {
|
|
103
|
+
input = JSON.parse(inputText);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
if (input === null || typeof input !== 'object')
|
|
109
|
+
return '';
|
|
110
|
+
const response = codexHookResponse(input, sessionInbox, env);
|
|
111
|
+
return response === undefined ? '' : `${JSON.stringify(response)}\n`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { notificationDeliveryWaitMs, processActNotificationsOnce, } from '../notifications.js';
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
let squarePath;
|
|
7
|
+
let actIndex;
|
|
8
|
+
for (let index = 0; index < argv.length; index++) {
|
|
9
|
+
const argument = argv[index];
|
|
10
|
+
if (argument === '--square-path' && argv[index + 1] !== undefined) {
|
|
11
|
+
squarePath = resolve(argv[++index]);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (argument === '--act-index' && argv[index + 1] !== undefined) {
|
|
15
|
+
const value = Number(argv[++index]);
|
|
16
|
+
if (Number.isInteger(value) && value >= 0)
|
|
17
|
+
actIndex = value;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unknown notify-once argument: ${argument}`);
|
|
21
|
+
}
|
|
22
|
+
if (!squarePath || actIndex === undefined) {
|
|
23
|
+
throw new Error('notify-once requires --square-path and --act-index.');
|
|
24
|
+
}
|
|
25
|
+
return { squarePath, actIndex };
|
|
26
|
+
}
|
|
27
|
+
async function main() {
|
|
28
|
+
if (process.env['SQUARE_DISABLE_PASEO_WAKE'] === '1')
|
|
29
|
+
return;
|
|
30
|
+
const { squarePath, actIndex } = parseArgs(process.argv.slice(2));
|
|
31
|
+
await sleep(notificationDeliveryWaitMs());
|
|
32
|
+
await processActNotificationsOnce(squarePath, actIndex);
|
|
33
|
+
}
|
|
34
|
+
main().catch(() => {
|
|
35
|
+
// Detached notification delivery must never surface as a CLI failure.
|
|
36
|
+
process.exitCode = 0;
|
|
37
|
+
});
|
package/dist/compact.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { loadSquare, renderArtifactAct } from './artifact.js';
|
|
3
|
+
import { SquareError } from './model.js';
|
|
4
|
+
import { withPathOutput } from './presentation.js';
|
|
5
|
+
import { withSquareLock, writeSquareDoc } from './runtime.js';
|
|
6
|
+
import { coreCompact } from './decisions.js';
|
|
7
|
+
function sidecarPath(squarePath) {
|
|
8
|
+
return squarePath.replace(/\.md$/, '') + '.archive.md';
|
|
9
|
+
}
|
|
10
|
+
export async function cmdCompact(squarePath, opts) {
|
|
11
|
+
try {
|
|
12
|
+
let archivedCount;
|
|
13
|
+
let keptCount;
|
|
14
|
+
const archive = sidecarPath(squarePath);
|
|
15
|
+
await withSquareLock(squarePath, () => {
|
|
16
|
+
const doc = loadSquare(squarePath);
|
|
17
|
+
const result = coreCompact(doc, opts.keep);
|
|
18
|
+
archivedCount = result.archived.length;
|
|
19
|
+
keptCount = result.doc.acts.length;
|
|
20
|
+
if (archivedCount > 0) {
|
|
21
|
+
const sidecarExists = fs.existsSync(archive);
|
|
22
|
+
const block = result.archived
|
|
23
|
+
.map((act, index) => renderArtifactAct(act, { first: !sidecarExists && index === 0 }))
|
|
24
|
+
.join('\n');
|
|
25
|
+
fs.appendFileSync(archive, block + '\n');
|
|
26
|
+
writeSquareDoc(squarePath, result.doc);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
const summary = ['✓ compacted', ` · archived ${archivedCount} acts`, ` · kept ${keptCount} acts`, ...(archivedCount > 0 ? [` · sidecar ${archive}`] : [])].join('\n');
|
|
30
|
+
process.stdout.write(withPathOutput(squarePath, summary));
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err instanceof SquareError) {
|
|
34
|
+
process.stderr.write(err.message + '\n');
|
|
35
|
+
process.exit(err.code === 'not_found' ? 1 : 2);
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { SquareError, sameName, validateName, } from './model.js';
|
|
2
|
+
import { UNREAD_BLOCK_GRACE_MS, actStableIndex, currentHold, foldedState, freshWatchLease, getReadState, publicActs, readCursor, resolveRosterName, rosterNames, extractMentions, matchesMentionTarget, isPostJoinActivity, isDeliveryDelivered, THROTTLE_WINDOW_MS, } from './runtime.js';
|
|
3
|
+
import { indexedDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
|
|
4
|
+
import { validate } from './square-core.js';
|
|
5
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
6
|
+
import { compileSearchPattern } from './search.js';
|
|
7
|
+
export function resolveKnownName(doc, name) {
|
|
8
|
+
validateName(name);
|
|
9
|
+
const known = resolveRosterName(doc, name);
|
|
10
|
+
if (known === undefined) {
|
|
11
|
+
const roster = rosterNames(doc);
|
|
12
|
+
throw new SquareError('invalid_args', `Unknown participant "${name}". Expected one of: ${roster.join(', ')}.`);
|
|
13
|
+
}
|
|
14
|
+
return known;
|
|
15
|
+
}
|
|
16
|
+
function participantState(state, name) {
|
|
17
|
+
return state.participants.find((participant) => sameName(participant.name, name));
|
|
18
|
+
}
|
|
19
|
+
export function decideJoin(doc, name, now) {
|
|
20
|
+
const knownName = resolveRosterName(doc, name);
|
|
21
|
+
const joinedName = knownName ?? name;
|
|
22
|
+
const state = foldedState(doc);
|
|
23
|
+
const result = validate(state, { kind: 'join', actor: joinedName, at: now });
|
|
24
|
+
if (!result.ok && result.reason === 'already_joined') {
|
|
25
|
+
throw new SquareError('conflict', `A participant named "${joinedName}" is already in this square.`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
joinedName,
|
|
29
|
+
addParticipant: knownName === undefined,
|
|
30
|
+
joinAct: { kind: 'join', actor: joinedName, at: now, body: '' },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const UNREAD_PREVIEW_LIMIT = 3;
|
|
34
|
+
export function decideAct(doc, input) {
|
|
35
|
+
const { now, force } = input;
|
|
36
|
+
const name = resolveKnownName(doc, input.name);
|
|
37
|
+
const body = input.body;
|
|
38
|
+
if (body.trim() === '')
|
|
39
|
+
throw new SquareError('invalid_args', 'act body cannot be empty');
|
|
40
|
+
const reach = input.reach;
|
|
41
|
+
const state = foldedState(doc);
|
|
42
|
+
const current = participantState(state, name);
|
|
43
|
+
const result = validate(state, { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}) }, { hardCap: doc.hardCap, throttlePerMinute: doc.throttlePerMinute, throttleWindowMs: THROTTLE_WINDOW_MS });
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
if (result.reason === 'done')
|
|
46
|
+
throw new SquareError('conflict', `${name} is done; rejoin to act again`);
|
|
47
|
+
if (result.reason === 'held')
|
|
48
|
+
return { type: 'held', reason: result.hold.reason };
|
|
49
|
+
if (result.reason === 'hard_cap')
|
|
50
|
+
return { type: 'capped', count: result.count, hardCap: result.hardCap };
|
|
51
|
+
if (result.reason === 'throttled')
|
|
52
|
+
return { type: 'throttled', delayMs: result.delayMs };
|
|
53
|
+
if (result.reason === 'bell_quota')
|
|
54
|
+
return { type: 'bell_quota', nextAt: result.nextAt };
|
|
55
|
+
if (result.reason === 'not_joined')
|
|
56
|
+
throw new SquareError('conflict', `${name} has not joined this square`);
|
|
57
|
+
}
|
|
58
|
+
const delta = indexedDelta(doc.acts, readCursor(doc, name));
|
|
59
|
+
const unreadPublic = peerPublicActs(delta, name);
|
|
60
|
+
const unreadRoomChanges = peerRoomChanges(delta, name);
|
|
61
|
+
const sayCountByActor = new Map();
|
|
62
|
+
const unreadByParticipant = new Map();
|
|
63
|
+
for (const item of delta) {
|
|
64
|
+
if (item.act.kind === 'say') {
|
|
65
|
+
const key = item.act.actor.toLocaleLowerCase();
|
|
66
|
+
sayCountByActor.set(key, (sayCountByActor.get(key) ?? 0) + 1);
|
|
67
|
+
}
|
|
68
|
+
if (item.act.kind !== 'say' || sameName(item.act.actor, name))
|
|
69
|
+
continue;
|
|
70
|
+
const actorKey = item.act.actor.toLocaleLowerCase();
|
|
71
|
+
const currentSummary = unreadByParticipant.get(item.act.actor);
|
|
72
|
+
unreadByParticipant.set(item.act.actor, {
|
|
73
|
+
count: (currentSummary?.count ?? 0) + 1,
|
|
74
|
+
latestAt: currentSummary === undefined ? item.act.at : Math.max(currentSummary.latestAt, item.act.at),
|
|
75
|
+
previews: [
|
|
76
|
+
...(currentSummary?.previews ?? []),
|
|
77
|
+
{ number: sayCountByActor.get(actorKey) ?? 1, act: item.act },
|
|
78
|
+
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const activitySummaries = [...unreadByParticipant.entries()]
|
|
82
|
+
.map(([participant, summary]) => ({
|
|
83
|
+
name: participant,
|
|
84
|
+
count: summary.count,
|
|
85
|
+
latestActivityAgeMs: Math.max(0, now - summary.latestAt),
|
|
86
|
+
previews: summary.previews,
|
|
87
|
+
}))
|
|
88
|
+
.sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
|
|
89
|
+
const latestActivityAgeMs = activitySummaries[0]?.latestActivityAgeMs;
|
|
90
|
+
const hasUnread = unreadPublic.length > 0 || unreadRoomChanges.length > 0;
|
|
91
|
+
const hasFreshUnreadActivity = latestActivityAgeMs !== undefined && latestActivityAgeMs <= UNREAD_BLOCK_GRACE_MS;
|
|
92
|
+
if (!force && hasUnread && !hasFreshUnreadActivity) {
|
|
93
|
+
return { type: 'blocked', activitySummaries, unreadRoomChanges };
|
|
94
|
+
}
|
|
95
|
+
const ownActCount = (current?.activityCount ?? 0) + 1;
|
|
96
|
+
return {
|
|
97
|
+
type: 'sent',
|
|
98
|
+
act: { kind: 'say', actor: name, at: now, body, ...(reach !== undefined ? { reach } : {}) },
|
|
99
|
+
confirmation: `● heads turn your way — #${ownActCount}`,
|
|
100
|
+
ownActCount,
|
|
101
|
+
pendingPublic: unreadPublic,
|
|
102
|
+
pendingRoomChanges: unreadRoomChanges,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function coreDone(doc, name, body, now) {
|
|
106
|
+
const resolvedName = resolveKnownName(doc, name);
|
|
107
|
+
return { kind: 'done', actor: resolvedName, at: now, body: body.replace(/\r\n/g, '\n').trim() };
|
|
108
|
+
}
|
|
109
|
+
export function coreHold(_doc, actor, body, now) {
|
|
110
|
+
return { kind: 'hold', actor, at: now, body: body.replace(/\r\n/g, '\n').trim() };
|
|
111
|
+
}
|
|
112
|
+
export function coreResume(_doc, actor, now) {
|
|
113
|
+
return { kind: 'resume', actor, at: now, body: '' };
|
|
114
|
+
}
|
|
115
|
+
export function corePresence(doc, now) {
|
|
116
|
+
const state = foldedState(doc);
|
|
117
|
+
return rosterNames(doc).map((participant) => {
|
|
118
|
+
const snapshot = participantState(state, participant);
|
|
119
|
+
if (snapshot?.done)
|
|
120
|
+
return { name: participant, state: 'done', lastAt: snapshot.lastActiveAt };
|
|
121
|
+
const cursor = getReadState(doc, participant);
|
|
122
|
+
const lease = freshWatchLease(doc, participant, now);
|
|
123
|
+
if (lease !== undefined)
|
|
124
|
+
return { name: participant, state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt };
|
|
125
|
+
const lastAt = cursor?.updatedAt ?? (snapshot?.joined ? snapshot.lastActiveAt : undefined);
|
|
126
|
+
if (lastAt === undefined)
|
|
127
|
+
return { name: participant, state: 'never-joined', lastAt: undefined };
|
|
128
|
+
return { name: participant, state: 'active', lastAt };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function buildParticipantStatuses(doc, now, state = foldedState(doc)) {
|
|
132
|
+
const delivery = deriveDeliveryModel(doc);
|
|
133
|
+
return state.participants.map(({ name: participant }) => {
|
|
134
|
+
const snapshot = participantState(state, participant);
|
|
135
|
+
const cursor = getReadState(doc, participant);
|
|
136
|
+
const lease = freshWatchLease(doc, participant, now);
|
|
137
|
+
const presence = snapshot?.done
|
|
138
|
+
? { state: 'done', lastAt: snapshot.lastActiveAt }
|
|
139
|
+
: lease !== undefined
|
|
140
|
+
? { state: 'watching', lastAt: cursor?.updatedAt ?? lease.heartbeatAt }
|
|
141
|
+
: cursor !== undefined || snapshot?.joined
|
|
142
|
+
? { state: 'active', lastAt: cursor?.updatedAt ?? snapshot?.lastActiveAt }
|
|
143
|
+
: { state: 'never-joined', lastAt: undefined };
|
|
144
|
+
const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
|
|
145
|
+
const consumedThrough = readCursor(doc, participant);
|
|
146
|
+
let unreadActivityCount = 0;
|
|
147
|
+
if (snapshot?.joined) {
|
|
148
|
+
for (const act of doc.acts) {
|
|
149
|
+
if (actStableIndex(act) <= consumedThrough || act.actor === undefined || sameName(act.actor, participant))
|
|
150
|
+
continue;
|
|
151
|
+
if (act.kind !== 'read')
|
|
152
|
+
unreadActivityCount++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
name: participant,
|
|
157
|
+
state: participantStatus,
|
|
158
|
+
activityCount: snapshot?.activityCount ?? 0,
|
|
159
|
+
lastActiveAt: snapshot?.lastActiveAt,
|
|
160
|
+
presence: presence.state,
|
|
161
|
+
presenceAt: presence.lastAt,
|
|
162
|
+
unreadActivityCount,
|
|
163
|
+
pendingMentionCount: snapshot?.joined ? delivery.pendingFor(participant).length : 0,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export function coreStatus(doc, now) {
|
|
168
|
+
const state = foldedState(doc);
|
|
169
|
+
const latestAct = publicActs(doc.acts).at(-1);
|
|
170
|
+
return {
|
|
171
|
+
hardCap: doc.hardCap,
|
|
172
|
+
throttlePerMinute: doc.throttlePerMinute,
|
|
173
|
+
participantCount: state.joined.length,
|
|
174
|
+
activeCount: state.joined.length,
|
|
175
|
+
doneCount: state.done.length,
|
|
176
|
+
holdActive: state.hold.active,
|
|
177
|
+
holdReason: state.hold.reason,
|
|
178
|
+
holdActor: state.hold.actor,
|
|
179
|
+
holdAt: state.hold.at,
|
|
180
|
+
participants: buildParticipantStatuses(doc, now, state),
|
|
181
|
+
latestAct,
|
|
182
|
+
now,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function coreParticipants(doc, now) {
|
|
186
|
+
return { participants: buildParticipantStatuses(doc, now), now };
|
|
187
|
+
}
|
|
188
|
+
/** True when a say is a bell or explicitly @viewer — not broadcast. */
|
|
189
|
+
function addressesViewer(act, viewer) {
|
|
190
|
+
if (act.kind !== 'say')
|
|
191
|
+
return false;
|
|
192
|
+
if (act.reach === 'bell')
|
|
193
|
+
return true;
|
|
194
|
+
return extractMentions(act.body).some((name) => sameName(name, viewer));
|
|
195
|
+
}
|
|
196
|
+
export function coreActivities(doc, opts) {
|
|
197
|
+
const participants = opts.participants ?? [];
|
|
198
|
+
const canonicalParticipants = participants.map((participant) => resolveKnownName(doc, participant));
|
|
199
|
+
const viewer = opts.viewer !== undefined ? resolveKnownName(doc, opts.viewer) : undefined;
|
|
200
|
+
let acts = doc.acts.map((act) => ({ act, index: actStableIndex(act) }));
|
|
201
|
+
// --at establishes a context window first; other filters AND inside it.
|
|
202
|
+
if (opts.atIndex != null) {
|
|
203
|
+
const before = opts.beforeContext ?? 0;
|
|
204
|
+
const after = opts.afterContext ?? 0;
|
|
205
|
+
const centerPos = acts.findIndex((item) => item.index === opts.atIndex);
|
|
206
|
+
if (centerPos < 0)
|
|
207
|
+
return [];
|
|
208
|
+
acts = acts.slice(Math.max(0, centerPos - before), centerPos + after + 1);
|
|
209
|
+
}
|
|
210
|
+
if (opts.ids !== undefined && opts.ids.length > 0) {
|
|
211
|
+
const wanted = new Set(opts.ids);
|
|
212
|
+
acts = acts.filter(({ index }) => wanted.has(index));
|
|
213
|
+
}
|
|
214
|
+
if (opts.afterIndex != null)
|
|
215
|
+
acts = acts.filter(({ index }) => index > opts.afterIndex);
|
|
216
|
+
if (canonicalParticipants.length > 0) {
|
|
217
|
+
acts = acts.filter(({ act }) => (act.kind === 'say' && act.reach === 'bell') ||
|
|
218
|
+
(act.actor !== undefined && canonicalParticipants.some((participant) => sameName(participant, act.actor))));
|
|
219
|
+
}
|
|
220
|
+
if (opts.before != null)
|
|
221
|
+
acts = acts.filter(({ act }) => act.at < opts.before);
|
|
222
|
+
if (opts.after != null)
|
|
223
|
+
acts = acts.filter(({ act }) => act.at > opts.after);
|
|
224
|
+
if (opts.mention != null) {
|
|
225
|
+
const mention = resolveKnownName(doc, opts.mention);
|
|
226
|
+
acts = acts.filter(({ act }) => act.kind === 'say' && (act.reach === 'bell' || matchesMentionTarget(act, mention)));
|
|
227
|
+
}
|
|
228
|
+
if (opts.mentionsViewer) {
|
|
229
|
+
if (viewer === undefined)
|
|
230
|
+
return [];
|
|
231
|
+
acts = acts.filter(({ act }) => addressesViewer(act, viewer));
|
|
232
|
+
}
|
|
233
|
+
if (opts.pending) {
|
|
234
|
+
if (viewer === undefined)
|
|
235
|
+
return [];
|
|
236
|
+
acts = acts.filter(({ act, index }) => {
|
|
237
|
+
if (!addressesViewer(act, viewer))
|
|
238
|
+
return false;
|
|
239
|
+
if (!isPostJoinActivity(doc.acts, viewer, index))
|
|
240
|
+
return false;
|
|
241
|
+
return !isDeliveryDelivered(doc, viewer, index);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
|
|
245
|
+
if (search !== undefined && search.pattern !== '') {
|
|
246
|
+
// Search output is defined over the same public say/done activities in every
|
|
247
|
+
// presentation mode, including --count, --json, and human-readable echo.
|
|
248
|
+
acts = acts.filter(({ act }) => act.kind === 'say' || act.kind === 'done');
|
|
249
|
+
const re = compileSearchPattern(search.pattern, search.fixed);
|
|
250
|
+
acts = acts.filter(({ act }) => 'body' in act && typeof act.body === 'string' && re.test(act.body));
|
|
251
|
+
}
|
|
252
|
+
if (opts.order === 'desc') {
|
|
253
|
+
acts = [...acts].sort((a, b) => b.index - a.index || b.act.at - a.act.at);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
acts = [...acts].sort((a, b) => a.index - b.index || a.act.at - b.act.at);
|
|
257
|
+
}
|
|
258
|
+
return acts;
|
|
259
|
+
}
|
|
260
|
+
export function coreCompact(doc, keep) {
|
|
261
|
+
if (doc.acts.length <= keep)
|
|
262
|
+
return { archived: [], doc };
|
|
263
|
+
const splitAt = doc.acts.length - keep;
|
|
264
|
+
const archived = doc.acts.slice(0, splitAt);
|
|
265
|
+
const retained = doc.acts.slice(splitAt);
|
|
266
|
+
const cutoffIndex = actStableIndex(archived[archived.length - 1]);
|
|
267
|
+
const unread = rosterNames(doc).filter((participant) => {
|
|
268
|
+
if (!resolveRosterName(doc, participant) || !currentHold(doc.acts))
|
|
269
|
+
return false;
|
|
270
|
+
if (!foldedState(doc).participants.some((entry) => sameName(entry.name, participant) && entry.joined))
|
|
271
|
+
return false;
|
|
272
|
+
return readCursor(doc, participant) < cutoffIndex;
|
|
273
|
+
});
|
|
274
|
+
if (unread.length > 0) {
|
|
275
|
+
throw new SquareError('conflict', `Refusing to compact: ${unread.join(', ')} ${unread.length === 1 ? 'has' : 'have'} not read through the acts being archived.`);
|
|
276
|
+
}
|
|
277
|
+
const firstActIndex = retained.length > 0 ? actStableIndex(retained[0]) : doc.runtime.nextActIndex;
|
|
278
|
+
return {
|
|
279
|
+
archived,
|
|
280
|
+
doc: {
|
|
281
|
+
...doc,
|
|
282
|
+
acts: retained,
|
|
283
|
+
runtime: { ...doc.runtime, firstActIndex },
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|