@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
package/dist/help.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const COMMANDS = [
|
|
2
|
+
{ names: ['help'], usage: 'help [command]', summary: 'Show the command index or help for one command.' },
|
|
3
|
+
{ names: ['version'], usage: 'version', summary: 'Print the installed version.' },
|
|
4
|
+
{
|
|
5
|
+
names: ['build'], usage: 'build --cap <N|-1> [--template <name>] [--throttle N] [-f] < body.md', usesSquare: true,
|
|
6
|
+
summary: 'Create a square from Markdown on stdin.',
|
|
7
|
+
details: ['Options:', ' --cap <N|-1> Required activity cap; -1 means unlimited.', ' --template <name> Append a packaged activity guide.', ' --throttle <N> Allow at most N public activities per minute.', ' -f, --force Replace an existing artifact.'],
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
names: ['ls', 'list'], usage: '{command} [--depth N]', summary: 'List nearby squares below the current directory.',
|
|
11
|
+
details: ['Options:', ' --depth <N> Descend through at most N directory levels (default 4; 0 scans only the current directory).'],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
names: ['join'], usage: '--as <name> join [--last N | --all]', usesSquare: true,
|
|
15
|
+
summary: 'Step into the square and read its current context.',
|
|
16
|
+
details: ['Options:', ' --last <N> Show the last N public activities (default 10).', ' --all Show the full public archive.'],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
names: ['act'], usage: '--as <name> act [-f|--force] [--no-wait] [--beside <name> | --bell] <activity | ->', usesSquare: true,
|
|
20
|
+
summary: 'Add one public activity; pass - to read a multi-line body from stdin.',
|
|
21
|
+
details: ['Options:', ' -f, --force Speak over older unread peer activity.', ' --no-wait Return through a hold or throttle lull, preserving a draft.', ' --beside <name> Let only that participant hear the full body.', ' --bell Ring every participant once within the bell window.'],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
names: ['catch'], usage: '--as <name> catch [--now | --follow] [--count N] [--by <names>] [--mention [name]] [--idle <duration>] [-f|--force]', usesSquare: true,
|
|
25
|
+
summary: 'Catch peer activity and advance this participant\'s presence.',
|
|
26
|
+
details: ['Options:', ' --now Catch up immediately.', ' --follow Keep catching until idle or a terminal state.', ' --count <N> Return after N matching peer activities.', ' --by <names> Match only comma-separated participants.', ' --mention [name] Match mentions of a name, or your own name when omitted.', ' --idle <duration> Set the idle wait, for example 30s, 10m, or 1h.', ' -f, --force Replace another active catch for this participant.'],
|
|
27
|
+
},
|
|
28
|
+
{ names: ['done'], usage: '--as <name> done [final | -]', usesSquare: true, summary: 'Step out, optionally leaving a final note.' },
|
|
29
|
+
{
|
|
30
|
+
names: ['stream'], usage: 'stream [--ndjson [--for <name>]]', usesSquare: true,
|
|
31
|
+
summary: 'Follow activity without consuming participant presence.',
|
|
32
|
+
details: ['Options:', ' --ndjson Emit one JSON event per line.', ' --for <name> With --ndjson, emit notifications for one participant.'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
names: ['inbox'], usage: 'inbox --for-session <session-id> [--json]',
|
|
36
|
+
summary: 'Inspect bounded machine-local notifications for a native session.',
|
|
37
|
+
details: ['Options:', ' --for-session <id> Required harness session id.', ' --json Emit structured JSON.'],
|
|
38
|
+
},
|
|
39
|
+
{ names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Run one native turn-boundary hook event from JSON on stdin.' },
|
|
40
|
+
{
|
|
41
|
+
names: ['echo'], usage: '[--as <name>] echo [filters] [output]', usesSquare: true,
|
|
42
|
+
summary: 'Read the activity archive without advancing participant presence.',
|
|
43
|
+
details: ['Filters:', ' --from, --by <names> Match participants.', ' --since, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search activity bodies.', ' --mention <name> Match mentions.', ' --mentions me | --pending Match attention for --as <name>.', ' --ids <ids> | --at <id> Match stable activity ids.', ' -B, -A, -C <N> Set non-negative context around --at.', ' --after <id> Match activities after an id.', ' --order <asc|desc> Set result order.', ' --last, --limit <N> | --all Bound the result count.', '', 'Output:', ' --full --json --format <fields> --count'],
|
|
44
|
+
},
|
|
45
|
+
{ names: ['warmup'], usage: 'warmup', usesSquare: true, summary: 'Print the complete embedded participant warmup.' },
|
|
46
|
+
{ names: ['status'], usage: '[--as <name>] status', usesSquare: true, summary: 'Show the current state and latest public activity.' },
|
|
47
|
+
{ names: ['participants'], usage: 'participants', usesSquare: true, summary: 'Show the participant roster and current states.' },
|
|
48
|
+
{ names: ['hold'], usage: '--as <name> hold [reason | -]', usesSquare: true, summary: 'Raise a hand and pause participant activity.' },
|
|
49
|
+
{ names: ['resume'], usage: '--as <name> resume', usesSquare: true, summary: 'Lower the raised hand and resume activity.' },
|
|
50
|
+
{
|
|
51
|
+
names: ['harness'], usage: 'harness <install <skills|claude|codex|opencode|pi> [-f] | uninstall codex | doctor [codex|opencode|delivery]>', usesSquare: true,
|
|
52
|
+
summary: 'Install, remove, or diagnose official harness adapters.',
|
|
53
|
+
},
|
|
54
|
+
{ names: ['compact'], usage: 'compact [--keep N]', usesSquare: true, summary: 'Archive older acts while retaining the latest N.' },
|
|
55
|
+
{
|
|
56
|
+
names: ['doctor'], usage: 'doctor [--fix] [reconcile-backlog [--before <time>]]', usesSquare: true,
|
|
57
|
+
summary: 'Diagnose artifact integrity and delivery health.',
|
|
58
|
+
details: ['Options:', ' --fix Repair recoverable artifact problems.', ' reconcile-backlog Close historical delivery acknowledgement debt.', ' --before <time> Record the intended reconciliation boundary.'],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
function definitionFor(command) {
|
|
62
|
+
return COMMANDS.find((item) => item.names.includes(command));
|
|
63
|
+
}
|
|
64
|
+
function isHelpFlag(value) {
|
|
65
|
+
return value === '--help' || value === '-h';
|
|
66
|
+
}
|
|
67
|
+
export function renderGlobalHelp() {
|
|
68
|
+
const commandLines = COMMANDS
|
|
69
|
+
.filter((item) => item.names[0] !== 'help')
|
|
70
|
+
.map((item) => ` ${item.names.join(', ')}\n ${item.summary}`);
|
|
71
|
+
return [
|
|
72
|
+
'Usage: square [--square-path <path>] [--as <name>] <command> [args...]',
|
|
73
|
+
'',
|
|
74
|
+
'Commands:',
|
|
75
|
+
...commandLines,
|
|
76
|
+
'',
|
|
77
|
+
"Run 'square <command> --help' for command options.",
|
|
78
|
+
'',
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
export function renderSubcommandHelp(command) {
|
|
82
|
+
const definition = definitionFor(command);
|
|
83
|
+
if (definition === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
const aliases = definition.names.filter((name) => name !== command);
|
|
86
|
+
const usage = definition.usage.replace('{command}', command);
|
|
87
|
+
return [
|
|
88
|
+
`Usage: square ${definition.usesSquare ? '[--square-path <path>] ' : ''}${usage}`,
|
|
89
|
+
...(aliases.length > 0 ? [`Aliases: ${aliases.join(', ')}`] : []),
|
|
90
|
+
'',
|
|
91
|
+
definition.summary,
|
|
92
|
+
...(definition.details === undefined ? [] : ['', ...definition.details]),
|
|
93
|
+
'',
|
|
94
|
+
'Help:',
|
|
95
|
+
' -h, --help Show this command help.',
|
|
96
|
+
'',
|
|
97
|
+
"Run 'square help' to list every command.",
|
|
98
|
+
'',
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
export function helpRequest(rawArgs) {
|
|
102
|
+
const args = [];
|
|
103
|
+
for (let index = 0; index < rawArgs.length; index++) {
|
|
104
|
+
const arg = rawArgs[index];
|
|
105
|
+
if (arg === '--square-path' || arg === '--as') {
|
|
106
|
+
const value = rawArgs[index + 1];
|
|
107
|
+
if (value === undefined || value.startsWith('--'))
|
|
108
|
+
return undefined;
|
|
109
|
+
index++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
args.push(arg);
|
|
113
|
+
}
|
|
114
|
+
if (isHelpFlag(args[0]))
|
|
115
|
+
return {};
|
|
116
|
+
if (args[0] === 'help') {
|
|
117
|
+
if (args.length === 1)
|
|
118
|
+
return {};
|
|
119
|
+
if (args.length === 2)
|
|
120
|
+
return { command: isHelpFlag(args[1]) ? 'help' : args[1] };
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
if (args.length > 1 && args.slice(1).some(isHelpFlag))
|
|
124
|
+
return { command: args[0] };
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
export function commandUsageHint(command) {
|
|
128
|
+
return command !== undefined && definitionFor(command) !== undefined
|
|
129
|
+
? `Run 'square ${command} --help' for usage.\n`
|
|
130
|
+
: "Run 'square help' to list commands.\n";
|
|
131
|
+
}
|
package/dist/inbox.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { loadSquare } from './artifact.js';
|
|
2
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
3
|
+
import { lookupSession } from './registry.js';
|
|
4
|
+
import { freshWatchLease, isCurrentlyJoined, resolveRosterName } from './runtime.js';
|
|
5
|
+
export function sessionInbox(sessionId) {
|
|
6
|
+
const inbox = [];
|
|
7
|
+
for (const membership of lookupSession(sessionId)) {
|
|
8
|
+
try {
|
|
9
|
+
const doc = loadSquare(membership.squarePath);
|
|
10
|
+
const name = resolveRosterName(doc, membership.name);
|
|
11
|
+
if (!name || !isCurrentlyJoined(doc.acts, name))
|
|
12
|
+
continue;
|
|
13
|
+
const notifications = deriveDeliveryModel(doc).pendingFor(name).map(({ item, via }) => ({
|
|
14
|
+
actIndex: item.index,
|
|
15
|
+
actor: item.act.actor,
|
|
16
|
+
at: item.act.at,
|
|
17
|
+
via,
|
|
18
|
+
body: item.act.body,
|
|
19
|
+
}));
|
|
20
|
+
const catchLease = freshWatchLease(doc, name);
|
|
21
|
+
inbox.push({
|
|
22
|
+
name,
|
|
23
|
+
squarePath: membership.squarePath,
|
|
24
|
+
notifications,
|
|
25
|
+
...(catchLease !== undefined ? { catchLease } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// A stale discovery-cache row only disables delivery for that membership.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return inbox;
|
|
33
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
export * from './model.js';
|
|
3
|
+
export * from './square-core.js';
|
|
4
|
+
export * from './decisions.js';
|
|
5
|
+
export { loadSquare } from './artifact.js';
|
|
6
|
+
export { indexedDelta } from './activity-feed.js';
|
|
7
|
+
export * from './notifications.js';
|
|
8
|
+
export * from './wake-sink.js';
|
|
9
|
+
export * from './paseo-timeline.js';
|
|
10
|
+
export * from './registry.js';
|
|
11
|
+
export * from './inbox.js';
|
|
12
|
+
export * from './presented.js';
|
|
13
|
+
export * from './delivery-health.js';
|
|
14
|
+
export * from './claude-hook.js';
|
|
15
|
+
export * from './harness.js';
|
|
16
|
+
export { extractMentions, countSays, joinedNames, doneNames, isCurrentlyJoined, publicActs, appendAct, withSquareLock, readCursor, } from './runtime.js';
|
|
17
|
+
import { loadSquare } from './artifact.js';
|
|
18
|
+
import { ackPeerDelta, indexedDelta, peerPublicActs, peerRoomChanges } from './activity-feed.js';
|
|
19
|
+
import { dispatchActNotifications, hasDeliveredMention as hasDeliveredMentionImpl, matchesMentionTarget, waitForDeliveredMention as waitForDeliveredMentionImpl, } from './notifications.js';
|
|
20
|
+
import { sameName } from './model.js';
|
|
21
|
+
import { SLEEP_MS, WATCH_HEARTBEAT_MS, WATCH_STALE_MS, actStableIndex, appendAct, freshWatchLease, getReadState as getDocReadState, latestIndexedActIndex, markDeliveredMentions, readCursor, withSquareLock, writeSquareDoc, } from './runtime.js';
|
|
22
|
+
import { decideAct, resolveKnownName } from './decisions.js';
|
|
23
|
+
export { WATCH_STALE_MS };
|
|
24
|
+
function actRefIndex(ref) {
|
|
25
|
+
if (typeof ref === 'number')
|
|
26
|
+
return ref;
|
|
27
|
+
const match = ref.match(/^act_(\d+)$/);
|
|
28
|
+
if (!match)
|
|
29
|
+
throw new Error(`Invalid act ref: ${ref}`);
|
|
30
|
+
return Number(match[1]);
|
|
31
|
+
}
|
|
32
|
+
function byList(by) {
|
|
33
|
+
if (by === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
return Array.isArray(by) ? by : by.split(',').map((item) => item.trim()).filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
function matchesProgrammaticFilter(item, opts) {
|
|
38
|
+
const speakers = byList(opts.by);
|
|
39
|
+
if (speakers !== undefined && (item.act.actor === undefined || !speakers.some((speaker) => sameName(speaker, item.act.actor))))
|
|
40
|
+
return false;
|
|
41
|
+
if (opts.mention !== undefined) {
|
|
42
|
+
if (item.act.kind !== 'say')
|
|
43
|
+
return false;
|
|
44
|
+
return matchesMentionTarget(item.act, opts.mention);
|
|
45
|
+
}
|
|
46
|
+
return item.act.kind === 'say' || item.act.kind === 'done' || item.act.kind === 'join';
|
|
47
|
+
}
|
|
48
|
+
export function getReadState(squarePath, name) {
|
|
49
|
+
const doc = loadSquare(squarePath);
|
|
50
|
+
return getDocReadState(doc, resolveKnownName(doc, name));
|
|
51
|
+
}
|
|
52
|
+
export function hasConsumedAct(squarePath, name, ref) {
|
|
53
|
+
const state = getReadState(squarePath, name);
|
|
54
|
+
return (state?.consumedThroughIndex ?? -1) >= actRefIndex(ref);
|
|
55
|
+
}
|
|
56
|
+
export function hasDeliveredMention(squarePath, name, ref) {
|
|
57
|
+
return hasDeliveredMentionImpl(squarePath, name, actRefIndex(ref));
|
|
58
|
+
}
|
|
59
|
+
export async function waitForDeliveredMention(squarePath, name, ref, opts = {}) {
|
|
60
|
+
return waitForDeliveredMentionImpl(squarePath, name, actRefIndex(ref), opts);
|
|
61
|
+
}
|
|
62
|
+
export function getParticipantPresence(squarePath, name, now = Date.now()) {
|
|
63
|
+
const doc = loadSquare(squarePath);
|
|
64
|
+
const known = resolveKnownName(doc, name);
|
|
65
|
+
const lease = freshWatchLease(doc, known, now);
|
|
66
|
+
return lease === undefined ? { watching: false } : { watching: true, lease };
|
|
67
|
+
}
|
|
68
|
+
export function isWatching(squarePath, name, now = Date.now()) {
|
|
69
|
+
return getParticipantPresence(squarePath, name, now).watching;
|
|
70
|
+
}
|
|
71
|
+
export async function act(squarePath, name, body, opts = {}) {
|
|
72
|
+
const sent = await withSquareLock(squarePath, () => {
|
|
73
|
+
const doc = loadSquare(squarePath);
|
|
74
|
+
const decision = decideAct(doc, { name, body, force: opts.force ?? false, now: Date.now() });
|
|
75
|
+
if (decision.type === 'sent') {
|
|
76
|
+
const appended = appendAct(squarePath, doc, decision.act);
|
|
77
|
+
return { act: appended, index: actStableIndex(appended) };
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Act rejected: ${decision.type}`);
|
|
80
|
+
});
|
|
81
|
+
if (sent)
|
|
82
|
+
await dispatchActNotifications(squarePath, sent);
|
|
83
|
+
}
|
|
84
|
+
export async function* streamEvents(squarePath, opts = {}) {
|
|
85
|
+
let doc = loadSquare(squarePath);
|
|
86
|
+
let cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
87
|
+
while (true) {
|
|
88
|
+
await sleep(SLEEP_MS);
|
|
89
|
+
try {
|
|
90
|
+
doc = loadSquare(squarePath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const delta = indexedDelta(doc.acts, cursor);
|
|
96
|
+
if (delta.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
cursor = latestIndexedActIndex(delta);
|
|
99
|
+
const filtered = delta.filter((item) => matchesProgrammaticFilter(item, opts));
|
|
100
|
+
if (filtered.length > 0)
|
|
101
|
+
yield filtered;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function* watch(squarePath, opts) {
|
|
105
|
+
let doc = loadSquare(squarePath);
|
|
106
|
+
const name = resolveKnownName(doc, opts.name);
|
|
107
|
+
const leaseId = `watch_api_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
108
|
+
let nextHeartbeatAt = Date.now() + WATCH_HEARTBEAT_MS;
|
|
109
|
+
await withSquareLock(squarePath, () => {
|
|
110
|
+
doc = loadSquare(squarePath);
|
|
111
|
+
const existing = freshWatchLease(doc, name);
|
|
112
|
+
if (existing !== undefined)
|
|
113
|
+
throw new Error(`${name} already has an active watch.`);
|
|
114
|
+
const at = Date.now();
|
|
115
|
+
nextHeartbeatAt = at + WATCH_HEARTBEAT_MS;
|
|
116
|
+
const participants = byList(opts.by);
|
|
117
|
+
const filter = { ...(participants ? { participants } : {}), ...(typeof opts.mention === 'string' ? { mention: opts.mention } : {}) };
|
|
118
|
+
doc.runtime.leases[name] = { leaseId, heartbeatAt: at, expiresAt: at + WATCH_STALE_MS, ...(Object.keys(filter).length > 0 ? { filter } : {}) };
|
|
119
|
+
writeSquareDoc(squarePath, doc);
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
while (true) {
|
|
123
|
+
const yielded = await withSquareLock(squarePath, () => {
|
|
124
|
+
const latest = loadSquare(squarePath);
|
|
125
|
+
const at = Date.now();
|
|
126
|
+
const lease = freshWatchLease(latest, name, at);
|
|
127
|
+
if (lease?.leaseId !== leaseId)
|
|
128
|
+
throw new Error(`${name}'s watch was replaced.`);
|
|
129
|
+
let mutated = false;
|
|
130
|
+
if (at >= nextHeartbeatAt) {
|
|
131
|
+
latest.runtime.leases[name] = { ...lease, heartbeatAt: at, expiresAt: at + WATCH_STALE_MS };
|
|
132
|
+
nextHeartbeatAt = at + WATCH_HEARTBEAT_MS;
|
|
133
|
+
mutated = true;
|
|
134
|
+
}
|
|
135
|
+
const delta = indexedDelta(latest.acts, readCursor(latest, name));
|
|
136
|
+
const deliverable = [...peerPublicActs(delta, name), ...peerRoomChanges(delta, name)];
|
|
137
|
+
if (deliverable.length === 0) {
|
|
138
|
+
if (mutated)
|
|
139
|
+
writeSquareDoc(squarePath, latest);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const filtered = deliverable.filter((item) => matchesProgrammaticFilter(item, opts));
|
|
143
|
+
const consumed = ackPeerDelta(latest, name, delta);
|
|
144
|
+
const receipts = markDeliveredMentions(latest, name, filtered);
|
|
145
|
+
if (consumed || receipts || mutated)
|
|
146
|
+
writeSquareDoc(squarePath, latest);
|
|
147
|
+
return filtered.length > 0 ? filtered : null;
|
|
148
|
+
});
|
|
149
|
+
if (yielded !== null)
|
|
150
|
+
yield yielded;
|
|
151
|
+
await sleep(SLEEP_MS);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await withSquareLock(squarePath, () => {
|
|
156
|
+
const latest = loadSquare(squarePath);
|
|
157
|
+
if (latest.runtime.leases[name]?.leaseId !== leaseId)
|
|
158
|
+
return;
|
|
159
|
+
delete latest.runtime.leases[name];
|
|
160
|
+
writeSquareDoc(squarePath, latest);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dist/list.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseSquare } from './artifact.js';
|
|
4
|
+
import { inSquareCount, publicActs } from './runtime.js';
|
|
5
|
+
import { formatRelativeTime } from './time.js';
|
|
6
|
+
const DEFAULT_LIST_DEPTH = 4;
|
|
7
|
+
const LIST_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']);
|
|
8
|
+
function frontmatterOf(text) {
|
|
9
|
+
const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
10
|
+
return match ? match[1] : null;
|
|
11
|
+
}
|
|
12
|
+
function candidateFrontmatter(filePath) {
|
|
13
|
+
let fd;
|
|
14
|
+
try {
|
|
15
|
+
fd = fs.openSync(filePath, 'r');
|
|
16
|
+
const buffer = Buffer.allocUnsafe(4096);
|
|
17
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
18
|
+
const prefix = buffer.toString('utf8', 0, bytesRead);
|
|
19
|
+
if (!prefix.startsWith('---\n'))
|
|
20
|
+
return null;
|
|
21
|
+
const frontmatter = frontmatterOf(prefix);
|
|
22
|
+
return frontmatter ?? frontmatterOf(fs.readFileSync(filePath, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
if (fd !== undefined)
|
|
29
|
+
fs.closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function readSquareListItem(filePath, root) {
|
|
33
|
+
let text;
|
|
34
|
+
let doc;
|
|
35
|
+
let stat;
|
|
36
|
+
try {
|
|
37
|
+
stat = fs.statSync(filePath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const frontmatter = candidateFrontmatter(filePath);
|
|
43
|
+
if (!frontmatter)
|
|
44
|
+
return null;
|
|
45
|
+
if (!/^hard_cap:\s*(-1|\d+)\s*$/m.test(frontmatter))
|
|
46
|
+
return null;
|
|
47
|
+
if (!/^format_version:\s*3\s*$/m.test(frontmatter))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
text = fs.readFileSync(filePath, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!text.includes('<!-- square:warmup -->') || !text.includes('<!-- square:activities -->'))
|
|
56
|
+
return null;
|
|
57
|
+
try {
|
|
58
|
+
doc = parseSquare(text);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const relative = path.relative(root, filePath) || path.basename(filePath);
|
|
64
|
+
return {
|
|
65
|
+
path: relative,
|
|
66
|
+
lastActiveAt: stat.mtimeMs,
|
|
67
|
+
participants: inSquareCount(doc),
|
|
68
|
+
activities: publicActs(doc.acts).filter((act) => act.kind === 'say').length,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function collectSquareList(root, maxDepth) {
|
|
72
|
+
const items = [];
|
|
73
|
+
function walk(dir, depth) {
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Directory vanished or became unreadable mid-walk — skip it, don't abort the scan.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = path.join(dir, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
if (depth < maxDepth && !LIST_SKIP_DIRS.has(entry.name))
|
|
86
|
+
walk(fullPath, depth + 1);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!entry.isFile())
|
|
90
|
+
continue;
|
|
91
|
+
const item = readSquareListItem(fullPath, root);
|
|
92
|
+
if (item)
|
|
93
|
+
items.push(item);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
walk(root, 0);
|
|
97
|
+
return items.sort((a, b) => a.path.localeCompare(b.path));
|
|
98
|
+
}
|
|
99
|
+
function renderSquareList(items) {
|
|
100
|
+
if (items.length === 0)
|
|
101
|
+
return '(no squares found)\n';
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const lines = [
|
|
104
|
+
'squares',
|
|
105
|
+
...items.map((item) => `${item.activities > 0 ? '●' : '○'} ${item.path} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants} in square · ${item.activities} acts`),
|
|
106
|
+
];
|
|
107
|
+
return lines.join('\n') + '\n';
|
|
108
|
+
}
|
|
109
|
+
function parseMaxDepth(args, usage) {
|
|
110
|
+
if (args.length === 0)
|
|
111
|
+
return DEFAULT_LIST_DEPTH;
|
|
112
|
+
if (args.length !== 2 || args[0] !== '--depth' || !/^\d+$/.test(args[1])) {
|
|
113
|
+
usage();
|
|
114
|
+
return DEFAULT_LIST_DEPTH;
|
|
115
|
+
}
|
|
116
|
+
const depth = Number(args[1]);
|
|
117
|
+
if (!Number.isSafeInteger(depth)) {
|
|
118
|
+
usage();
|
|
119
|
+
return DEFAULT_LIST_DEPTH;
|
|
120
|
+
}
|
|
121
|
+
return depth;
|
|
122
|
+
}
|
|
123
|
+
export function cmdListSquares(args, usage) {
|
|
124
|
+
const maxDepth = parseMaxDepth(args, usage);
|
|
125
|
+
process.stdout.write(renderSquareList(collectSquareList(process.cwd(), maxDepth)));
|
|
126
|
+
}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Shared model and constants for Square.
|
|
2
|
+
export class SquareError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'SquareError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export const WARMUP_HEADING = '## Warmup';
|
|
11
|
+
export const WARMUP_MARKER = '<!-- square:warmup -->';
|
|
12
|
+
export const ACTIVITIES_HEADING = '## Activities';
|
|
13
|
+
export const ACTIVITIES_MARKER = '<!-- square:activities -->';
|
|
14
|
+
export const ACT_MARKER_PREFIX = '<!-- square:act';
|
|
15
|
+
export const CURRENT_FORMAT_VERSION = 3;
|
|
16
|
+
export function formatParticipants(participants) {
|
|
17
|
+
return participants.join(', ');
|
|
18
|
+
}
|
|
19
|
+
export function formatHardCap(hardCap) {
|
|
20
|
+
return hardCap === null ? '-1' : String(hardCap);
|
|
21
|
+
}
|
|
22
|
+
export function parseParticipantList(value) {
|
|
23
|
+
return value
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((name) => name.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
export function nameKey(name) {
|
|
29
|
+
return name.toLocaleLowerCase();
|
|
30
|
+
}
|
|
31
|
+
export function sameName(a, b) {
|
|
32
|
+
return nameKey(a) === nameKey(b);
|
|
33
|
+
}
|
|
34
|
+
export function findParticipantName(participants, name) {
|
|
35
|
+
return participants.find((participant) => sameName(participant, name));
|
|
36
|
+
}
|
|
37
|
+
export function validateName(name) {
|
|
38
|
+
if (!name || !/^[\p{L}\p{N}_-]+$/u.test(name)) {
|
|
39
|
+
throw new SquareError('invalid_name', 'Invalid name: names must be non-empty and can only contain Unicode letters, digits, hyphens, and underscores.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function validateParticipantName(name) {
|
|
43
|
+
validateName(name);
|
|
44
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { loadSquare } from './artifact.js';
|
|
5
|
+
import { SquareError } from './model.js';
|
|
6
|
+
import { hasPresentedAttention } from './presented.js';
|
|
7
|
+
import { SLEEP_MS, isDeliveryDelivered, resolveRosterName, rosterNames, } from './runtime.js';
|
|
8
|
+
import { planActNotifications } from './delivery.js';
|
|
9
|
+
import { defaultWakeSinks, } from './wake-sink.js';
|
|
10
|
+
export { planActNotifications } from './delivery.js';
|
|
11
|
+
export { matchesMentionTarget } from './runtime.js';
|
|
12
|
+
function parsePositiveIntegerEnv(name, fallback) {
|
|
13
|
+
const raw = process.env[name];
|
|
14
|
+
if (raw === undefined)
|
|
15
|
+
return fallback;
|
|
16
|
+
const value = Number.parseInt(raw, 10);
|
|
17
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
18
|
+
throw new SquareError('invalid_args', `Invalid ${name}: expected a positive integer.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function resolveKnownParticipant(doc, name) {
|
|
23
|
+
const known = resolveRosterName(doc, name);
|
|
24
|
+
if (known === undefined) {
|
|
25
|
+
throw new SquareError('invalid_args', `Unknown participant "${name}". Expected one of: ${rosterNames(doc).join(', ')}.`);
|
|
26
|
+
}
|
|
27
|
+
return known;
|
|
28
|
+
}
|
|
29
|
+
export function notificationDeliveryWaitMs() {
|
|
30
|
+
return parsePositiveIntegerEnv('SQUARE_NOTIFY_DELIVERY_WAIT_MS', 5000);
|
|
31
|
+
}
|
|
32
|
+
export function hasDeliveredMention(squarePath, name, ref) {
|
|
33
|
+
const doc = loadSquare(squarePath);
|
|
34
|
+
const known = resolveKnownParticipant(doc, name);
|
|
35
|
+
const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
|
|
36
|
+
return isDeliveryDelivered(doc, known, index);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Duplicate-wake suppression only. Delivery remains pending until the canonical
|
|
40
|
+
* recipient/act receipt is delivered; presented is a machine-local cache.
|
|
41
|
+
*/
|
|
42
|
+
export function hasAttentionMention(squarePath, name, ref, env = process.env) {
|
|
43
|
+
const doc = loadSquare(squarePath);
|
|
44
|
+
const known = resolveKnownParticipant(doc, name);
|
|
45
|
+
const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
|
|
46
|
+
if (isDeliveryDelivered(doc, known, index))
|
|
47
|
+
return true;
|
|
48
|
+
return hasPresentedAttention(squarePath, known, index, env);
|
|
49
|
+
}
|
|
50
|
+
export async function waitForDeliveredMention(squarePath, name, ref, opts = {}) {
|
|
51
|
+
const timeoutMs = opts.timeoutMs ?? 30000;
|
|
52
|
+
const deadline = Date.now() + timeoutMs;
|
|
53
|
+
while (Date.now() <= deadline) {
|
|
54
|
+
if (hasDeliveredMention(squarePath, name, ref))
|
|
55
|
+
return true;
|
|
56
|
+
await sleep(Math.min(SLEEP_MS, Math.max(1, deadline - Date.now())));
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
61
|
+
const doc = loadSquare(squarePath);
|
|
62
|
+
const act = doc.acts.find((candidate) => candidate.index === actIndex);
|
|
63
|
+
if (!act)
|
|
64
|
+
return;
|
|
65
|
+
const item = { act, index: actIndex };
|
|
66
|
+
const notifications = planActNotifications(doc, item).filter((notification) => notification.via === 'mention' || notification.via === 'bell');
|
|
67
|
+
const sinks = opts.sinks ?? defaultWakeSinks();
|
|
68
|
+
if (sinks.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
for (const notification of notifications) {
|
|
71
|
+
// Presented only avoids duplicate wake text. It does not affect delivery state.
|
|
72
|
+
if (hasAttentionMention(squarePath, notification.recipient, actIndex))
|
|
73
|
+
continue;
|
|
74
|
+
for (const sink of sinks) {
|
|
75
|
+
await sink.dispatch(notification, { squarePath });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function launchDetachedWorker(workerPath, args) {
|
|
80
|
+
const child = spawn(process.execPath, [workerPath, ...args], {
|
|
81
|
+
detached: true,
|
|
82
|
+
stdio: 'ignore',
|
|
83
|
+
env: process.env,
|
|
84
|
+
});
|
|
85
|
+
child.unref();
|
|
86
|
+
}
|
|
87
|
+
export async function dispatchActNotifications(squarePath, item, opts = {}) {
|
|
88
|
+
if (process.env['SQUARE_DISABLE_PASEO_WAKE'] === '1')
|
|
89
|
+
return;
|
|
90
|
+
const doc = loadSquare(squarePath);
|
|
91
|
+
const notifications = planActNotifications(doc, item).filter((notification) => notification.via === 'mention' || notification.via === 'bell');
|
|
92
|
+
if (notifications.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
|
|
95
|
+
const launch = opts.launchWorker ?? launchDetachedWorker;
|
|
96
|
+
launch(workerPath, ['--square-path', squarePath, '--act-index', String(item.index)]);
|
|
97
|
+
}
|