@astrosheep/square 0.3.4 → 0.3.6
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 +3 -2
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +23 -22
- package/dist/artifact.js +126 -202
- package/dist/claude-hook.js +45 -21
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +76 -0
- package/dist/cli/meta-commands.js +28 -0
- package/dist/cli/observation-commands.js +453 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +219 -0
- package/dist/cmd/notify-once.js +23 -21
- package/dist/compact.js +6 -19
- package/dist/decisions.js +53 -86
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +68 -0
- package/dist/harness-codex.js +119 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +94 -576
- package/dist/help.js +44 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +30 -129
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +55 -63
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +26 -137
- package/dist/square-application.js +264 -0
- package/dist/square-core.js +3 -11
- package/dist/square.js +5 -1362
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +134 -188
- package/dist/watch.js +79 -138
- package/extensions/square-opencode.js +1 -1
- package/extensions/square-pi.js +8 -130
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/terminal.js +0 -125
package/dist/help.js
CHANGED
|
@@ -1,61 +1,62 @@
|
|
|
1
1
|
const COMMANDS = [
|
|
2
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.' },
|
|
3
|
+
{ names: ['version'], usage: 'version', summary: 'Print the installed version.', hiddenFromIndex: true },
|
|
4
4
|
{
|
|
5
|
-
names: ['build'], usage: 'build --cap <N
|
|
5
|
+
names: ['build'], usage: 'build [--cap <N|unlimited>] [--template <name>] [--throttle N] [-f] < body.md', usesSquare: true, group: 'host',
|
|
6
6
|
summary: 'Create a square from Markdown on stdin.',
|
|
7
|
-
details: ['Options:', ' --cap <N
|
|
7
|
+
details: ['Options:', ' --cap <N|unlimited> Set a per-participant activity cap (default 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
8
|
},
|
|
9
9
|
{
|
|
10
|
-
names: ['
|
|
10
|
+
names: ['list', 'ls'], usage: '{command} [--depth N]', summary: 'List nearby squares below the current directory.', group: 'host',
|
|
11
11
|
details: ['Options:', ' --depth <N> Descend through at most N directory levels (default 4; 0 scans only the current directory).'],
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
names: ['join'], usage: '--as <name> join [--last N | --all]', usesSquare: true,
|
|
14
|
+
names: ['join'], usage: '--as <name> join [--last N | --all]', usesSquare: true, group: 'participant',
|
|
15
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
|
|
16
|
+
details: ['Options:', ' --last <N> Show the last N public activities (default 10).', ' --all Show the complete history.'],
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
names: ['
|
|
20
|
-
summary: '
|
|
21
|
-
details: ['Options:', ' -f, --force
|
|
19
|
+
names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--beside <name> | --bell] <activity | ->', usesSquare: true, group: 'participant',
|
|
20
|
+
summary: 'Speak, gesture, or do both.',
|
|
21
|
+
details: ['Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --beside <name> Speak aside to one participant.', " --bell Call every participant's attention to this activity."],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
names: ['catch'], usage: '--as <name> catch
|
|
25
|
-
summary: 'Catch
|
|
26
|
-
details: ['
|
|
24
|
+
names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention [name]] [--replace]', usesSquare: true, group: 'participant',
|
|
25
|
+
summary: 'Catch what others have said or done since you last looked.',
|
|
26
|
+
details: ['Modes:', ' --now Catch up immediately.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention [name] Match mentions of a name, or your own name when omitted.', '', 'Recovery:', ' --replace Replace another active catch for this participant.'],
|
|
27
27
|
},
|
|
28
|
-
{ names: ['done'], usage: '--as <name> done [final | -]', usesSquare: true, summary: 'Step out, optionally leaving a final note.' },
|
|
28
|
+
{ names: ['done'], usage: '--as <name> done [final | -]', usesSquare: true, group: 'participant', summary: 'Step out, optionally leaving a final note.' },
|
|
29
29
|
{
|
|
30
|
-
names: ['stream'], usage: 'stream [--ndjson [--for <name>]]', usesSquare: true,
|
|
30
|
+
names: ['stream'], usage: 'stream [--ndjson [--for <name>]]', usesSquare: true, hiddenFromIndex: true,
|
|
31
31
|
summary: 'Follow activity without consuming participant presence.',
|
|
32
32
|
details: ['Options:', ' --ndjson Emit one JSON event per line.', ' --for <name> With --ndjson, emit notifications for one participant.'],
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
|
-
names: ['inbox'], usage: 'inbox --for-session <session-id> [--json]',
|
|
35
|
+
names: ['inbox'], usage: 'inbox --for-session <session-id> [--json]', hiddenFromIndex: true,
|
|
36
36
|
summary: 'Inspect bounded machine-local notifications for a native session.',
|
|
37
37
|
details: ['Options:', ' --for-session <id> Required harness session id.', ' --json Emit structured JSON.'],
|
|
38
38
|
},
|
|
39
|
-
{ names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Run one native turn-boundary hook event from JSON on stdin.' },
|
|
39
|
+
{ names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Run one native turn-boundary hook event from JSON on stdin.', hiddenFromIndex: true },
|
|
40
40
|
{
|
|
41
|
-
names: ['
|
|
42
|
-
summary: 'Read
|
|
43
|
-
details: ['Filters:', ' --from
|
|
41
|
+
names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
|
|
42
|
+
summary: 'Read or search what happened without changing what you have caught.',
|
|
43
|
+
details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time>, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search ids, participant names, and bodies.', ' --mention <name> Match mentions.', ' --pending Match attention waiting 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.', '', 'Results:', ' --limit <N> | --all Bound the newest matches (default 10).', ' --order <asc|desc> Set display order.', '', 'Output:', ' --full --json --format <fields> --count'],
|
|
44
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
|
|
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.' },
|
|
45
|
+
{ names: ['warmup'], usage: 'warmup', usesSquare: true, group: 'host', summary: 'Print the complete embedded participant warmup.' },
|
|
46
|
+
{ names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
|
|
47
|
+
{ names: ['participants'], usage: 'participants', usesSquare: true, group: 'host', summary: 'Show the full participant roster and current states.' },
|
|
48
|
+
{ names: ['hold'], usage: '--as <name> hold [reason | -]', usesSquare: true, group: 'participant', summary: 'Raise a hand and pause participant activity.' },
|
|
49
|
+
{ names: ['resume'], usage: '--as <name> resume', usesSquare: true, group: 'participant', summary: 'Lower the raised hand and resume activity.' },
|
|
50
50
|
{
|
|
51
|
-
names: ['harness'], usage: 'harness <install <skills|claude|codex|opencode|pi> [-f] | uninstall codex | doctor [codex|opencode|delivery]>', usesSquare: true,
|
|
51
|
+
names: ['harness'], usage: 'harness <install <skills|claude|codex|opencode|pi> [-f] | uninstall <skills|claude|codex|opencode|pi> | doctor [skills|claude|codex|opencode|pi|delivery]>', usesSquare: true, group: 'maintenance',
|
|
52
52
|
summary: 'Install, remove, or diagnose official harness adapters.',
|
|
53
|
+
details: ['Targets:', ' skills, claude, codex, opencode, pi Install, remove, or diagnose one adapter.', ' delivery Diagnose delivery only; skips when no readable artifact is selected.'],
|
|
53
54
|
},
|
|
54
|
-
{ names: ['compact'], usage: 'compact [--keep N]', usesSquare: true, summary: '
|
|
55
|
+
{ names: ['compact'], usage: 'compact [--keep N]', usesSquare: true, group: 'host', summary: 'Move older activity out of the working artifact while keeping the latest N.' },
|
|
55
56
|
{
|
|
56
|
-
names: ['doctor'], usage: 'doctor [--fix]
|
|
57
|
-
summary: 'Diagnose artifact integrity
|
|
58
|
-
details: ['Options:', ' --fix Repair recoverable artifact problems.'
|
|
57
|
+
names: ['doctor'], usage: 'doctor [--fix]', usesSquare: true, group: 'maintenance',
|
|
58
|
+
summary: 'Diagnose artifact integrity.',
|
|
59
|
+
details: ['Options:', ' --fix Repair recoverable artifact problems.'],
|
|
59
60
|
},
|
|
60
61
|
];
|
|
61
62
|
function definitionFor(command) {
|
|
@@ -65,15 +66,23 @@ function isHelpFlag(value) {
|
|
|
65
66
|
return value === '--help' || value === '-h';
|
|
66
67
|
}
|
|
67
68
|
export function renderGlobalHelp() {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const groups = [
|
|
70
|
+
{ key: 'participant', title: 'In the square:', order: ['join', 'express', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
|
|
71
|
+
{ key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants', 'warmup', 'compact'] },
|
|
72
|
+
{ key: 'maintenance', title: 'Setup and repair:', order: ['harness', 'doctor'] },
|
|
73
|
+
];
|
|
74
|
+
const commandLines = groups.flatMap(({ key, title, order }) => [
|
|
75
|
+
title,
|
|
76
|
+
...COMMANDS
|
|
77
|
+
.filter((item) => item.group === key && item.hiddenFromIndex !== true)
|
|
78
|
+
.sort((left, right) => order.indexOf(left.names[0]) - order.indexOf(right.names[0]))
|
|
79
|
+
.map((item) => ` ${item.names[0]}\n ${item.summary}`),
|
|
80
|
+
'',
|
|
81
|
+
]);
|
|
71
82
|
return [
|
|
72
83
|
'Usage: square [--square-path <path>] [--as <name>] <command> [args...]',
|
|
73
84
|
'',
|
|
74
|
-
'Commands:',
|
|
75
85
|
...commandLines,
|
|
76
|
-
'',
|
|
77
86
|
"Run 'square <command> --help' for command options.",
|
|
78
87
|
'',
|
|
79
88
|
].join('\n');
|
|
@@ -94,7 +103,7 @@ export function renderSubcommandHelp(command) {
|
|
|
94
103
|
'Help:',
|
|
95
104
|
' -h, --help Show this command help.',
|
|
96
105
|
'',
|
|
97
|
-
"Run 'square help' to list
|
|
106
|
+
"Run 'square help' to list available commands.",
|
|
98
107
|
'',
|
|
99
108
|
].join('\n');
|
|
100
109
|
}
|
package/dist/inbox.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import { loadSquare } from './artifact.js';
|
|
2
2
|
import { deriveDeliveryModel } from './delivery.js';
|
|
3
|
-
import {
|
|
3
|
+
import { lookupSessionBindings } from './registry.js';
|
|
4
4
|
import { freshWatchLease, isCurrentlyJoined, resolveRosterName } from './runtime.js';
|
|
5
5
|
export function sessionInbox(sessionId) {
|
|
6
6
|
const inbox = [];
|
|
7
|
-
for (const
|
|
7
|
+
for (const binding of lookupSessionBindings(sessionId)) {
|
|
8
8
|
try {
|
|
9
|
-
const doc = loadSquare(
|
|
10
|
-
const name = resolveRosterName(doc,
|
|
9
|
+
const doc = loadSquare(binding.squarePath);
|
|
10
|
+
const name = resolveRosterName(doc, binding.name);
|
|
11
11
|
if (!name || !isCurrentlyJoined(doc.acts, name))
|
|
12
12
|
continue;
|
|
13
|
-
const notifications = deriveDeliveryModel(doc).pendingFor(name).map(({ item,
|
|
13
|
+
const notifications = deriveDeliveryModel(doc).pendingFor(name).map(({ item, route }) => ({
|
|
14
14
|
actIndex: item.index,
|
|
15
|
-
actor: item.
|
|
16
|
-
at: item.
|
|
17
|
-
|
|
18
|
-
body: item.
|
|
15
|
+
actor: item.actor,
|
|
16
|
+
at: item.at,
|
|
17
|
+
route,
|
|
18
|
+
body: item.body,
|
|
19
19
|
}));
|
|
20
|
-
const
|
|
20
|
+
const lease = freshWatchLease(doc, name);
|
|
21
|
+
const catchLease = lease?.ownerId === binding.ownerId ? lease : undefined;
|
|
21
22
|
inbox.push({
|
|
22
23
|
name,
|
|
23
|
-
squarePath:
|
|
24
|
+
squarePath: binding.squarePath,
|
|
24
25
|
notifications,
|
|
25
26
|
...(catchLease !== undefined ? { catchLease } : {}),
|
|
26
27
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './model.js';
|
|
3
|
-
export * from './square-core.js';
|
|
4
|
-
export * from './decisions.js';
|
|
1
|
+
export { SquareError } from './model.js';
|
|
5
2
|
export { loadSquare } from './artifact.js';
|
|
6
|
-
export {
|
|
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';
|
|
3
|
+
export { extractMentions, countSays, joinedNames, doneNames, isCurrentlyJoined, publicActs, readCursor, } from './runtime.js';
|
|
17
4
|
import { loadSquare } from './artifact.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { decideAct, resolveKnownName } from './decisions.js';
|
|
5
|
+
import { hasDeliveredNotification as hasDeliveredNotificationImpl, waitForDeliveredNotification as waitForDeliveredNotificationImpl, } from './notifications.js';
|
|
6
|
+
import { WATCH_STALE_MS, freshWatchLease, getReadState as getDocReadState, } from './runtime.js';
|
|
7
|
+
import { resolveKnownName } from './decisions.js';
|
|
8
|
+
import { execute } from './square-application.js';
|
|
23
9
|
export { WATCH_STALE_MS };
|
|
24
10
|
function actRefIndex(ref) {
|
|
25
11
|
if (typeof ref === 'number')
|
|
@@ -29,22 +15,6 @@ function actRefIndex(ref) {
|
|
|
29
15
|
throw new Error(`Invalid act ref: ${ref}`);
|
|
30
16
|
return Number(match[1]);
|
|
31
17
|
}
|
|
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
18
|
export function getReadState(squarePath, name) {
|
|
49
19
|
const doc = loadSquare(squarePath);
|
|
50
20
|
return getDocReadState(doc, resolveKnownName(doc, name));
|
|
@@ -53,11 +23,11 @@ export function hasConsumedAct(squarePath, name, ref) {
|
|
|
53
23
|
const state = getReadState(squarePath, name);
|
|
54
24
|
return (state?.consumedThroughIndex ?? -1) >= actRefIndex(ref);
|
|
55
25
|
}
|
|
56
|
-
export function
|
|
57
|
-
return
|
|
26
|
+
export function hasDeliveredNotification(squarePath, name, ref) {
|
|
27
|
+
return hasDeliveredNotificationImpl(squarePath, name, actRefIndex(ref));
|
|
58
28
|
}
|
|
59
|
-
export async function
|
|
60
|
-
return
|
|
29
|
+
export async function waitForDeliveredNotification(squarePath, name, ref, opts = {}) {
|
|
30
|
+
return waitForDeliveredNotificationImpl(squarePath, name, actRefIndex(ref), opts);
|
|
61
31
|
}
|
|
62
32
|
export function getParticipantPresence(squarePath, name, now = Date.now()) {
|
|
63
33
|
const doc = loadSquare(squarePath);
|
|
@@ -68,96 +38,27 @@ export function getParticipantPresence(squarePath, name, now = Date.now()) {
|
|
|
68
38
|
export function isWatching(squarePath, name, now = Date.now()) {
|
|
69
39
|
return getParticipantPresence(squarePath, name, now).watching;
|
|
70
40
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
if (sent)
|
|
82
|
-
await dispatchActNotifications(squarePath, sent);
|
|
41
|
+
/** Typed participant intents share the same commit/effect pipeline as the CLI. */
|
|
42
|
+
export async function join(squarePath, name) {
|
|
43
|
+
await execute(squarePath, { type: 'join', name, now: Date.now() });
|
|
44
|
+
}
|
|
45
|
+
export async function done(squarePath, name, body = '') {
|
|
46
|
+
await execute(squarePath, { type: 'done', name, body, now: Date.now() });
|
|
47
|
+
}
|
|
48
|
+
export async function hold(squarePath, actor, body = '') {
|
|
49
|
+
await execute(squarePath, { type: 'hold', actor, body, now: Date.now() });
|
|
83
50
|
}
|
|
84
|
-
export async function
|
|
85
|
-
|
|
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
|
-
}
|
|
51
|
+
export async function resume(squarePath, actor) {
|
|
52
|
+
await execute(squarePath, { type: 'resume', actor, now: Date.now() });
|
|
103
53
|
}
|
|
104
|
-
export async function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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);
|
|
54
|
+
export async function express(squarePath, name, body, opts = {}) {
|
|
55
|
+
const committed = await execute(squarePath, {
|
|
56
|
+
type: 'say',
|
|
57
|
+
name,
|
|
58
|
+
body,
|
|
59
|
+
force: opts.force ?? false,
|
|
60
|
+
now: Date.now(),
|
|
120
61
|
});
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
}
|
|
62
|
+
if (committed.result.type !== 'sent')
|
|
63
|
+
throw new Error(`Activity rejected: ${committed.result.type}`);
|
|
163
64
|
}
|
package/dist/list.js
CHANGED
|
@@ -102,7 +102,7 @@ function renderSquareList(items) {
|
|
|
102
102
|
const now = Date.now();
|
|
103
103
|
const lines = [
|
|
104
104
|
'squares',
|
|
105
|
-
...items.map((item) => `${item.activities > 0 ? '●' : '○'} ${item.path} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants} in square · ${item.activities}
|
|
105
|
+
...items.map((item) => `${item.activities > 0 ? '●' : '○'} ${item.path} · ${formatRelativeTime(item.lastActiveAt, now)} · ${item.participants} in square · ${item.activities} activities`),
|
|
106
106
|
];
|
|
107
107
|
return lines.join('\n') + '\n';
|
|
108
108
|
}
|
package/dist/model.js
CHANGED
|
@@ -13,9 +13,6 @@ export const ACTIVITIES_HEADING = '## Activities';
|
|
|
13
13
|
export const ACTIVITIES_MARKER = '<!-- square:activities -->';
|
|
14
14
|
export const ACT_MARKER_PREFIX = '<!-- square:act';
|
|
15
15
|
export const CURRENT_FORMAT_VERSION = 3;
|
|
16
|
-
export function formatParticipants(participants) {
|
|
17
|
-
return participants.join(', ');
|
|
18
|
-
}
|
|
19
16
|
export function formatHardCap(hardCap) {
|
|
20
17
|
return hardCap === null ? '-1' : String(hardCap);
|
|
21
18
|
}
|
|
@@ -39,6 +36,3 @@ export function validateName(name) {
|
|
|
39
36
|
throw new SquareError('invalid_name', 'Invalid name: names must be non-empty and can only contain Unicode letters, digits, hyphens, and underscores.');
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
|
-
export function validateParticipantName(name) {
|
|
43
|
-
validateName(name);
|
|
44
|
-
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MAX_BYTES = 1_000_000;
|
|
4
|
+
export function notificationFailuresPath(squarePath, env = process.env) {
|
|
5
|
+
return env.SQUARE_NOTIFICATION_FAILURES ?? path.join(path.dirname(squarePath), 'notification-failures.ndjsonl');
|
|
6
|
+
}
|
|
7
|
+
function redact(value, secret = process.env.PASEO_PASSWORD) {
|
|
8
|
+
if (typeof value === 'string')
|
|
9
|
+
return secret ? value.split(secret).join('[redacted]') : value;
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.map((item) => redact(item, secret));
|
|
12
|
+
if (value !== null && typeof value === 'object') {
|
|
13
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item, secret)]));
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function parseRows(file) {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(file, 'utf8').split('\n').flatMap((line) => {
|
|
20
|
+
if (line.trim() === '')
|
|
21
|
+
return [];
|
|
22
|
+
try {
|
|
23
|
+
const row = JSON.parse(line);
|
|
24
|
+
return row.v === 1 && row.op === 'failed' && typeof row.actIndex === 'number' && typeof row.sink === 'string' && typeof row.message === 'string' && typeof row.at === 'number'
|
|
25
|
+
? [{ actIndex: row.actIndex, recipient: row.recipient, route: row.route, sink: row.sink, message: row.message, at: row.at, ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }) }]
|
|
26
|
+
: [];
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code === 'ENOENT')
|
|
35
|
+
return [];
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function readNotificationFailures(squarePath, env = process.env) {
|
|
40
|
+
return parseRows(notificationFailuresPath(squarePath, env));
|
|
41
|
+
}
|
|
42
|
+
/** Append diagnosable delivery failures without ever persisting Paseo credentials. */
|
|
43
|
+
export function recordNotificationFailure(squarePath, input, at = Date.now(), env = process.env) {
|
|
44
|
+
const file = notificationFailuresPath(squarePath, env);
|
|
45
|
+
const safe = redact({ ...input, at }, env.PASEO_PASSWORD);
|
|
46
|
+
const row = { v: 1, op: 'failed', ...safe };
|
|
47
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
48
|
+
const text = `${JSON.stringify(row)}\n`;
|
|
49
|
+
if (fs.existsSync(file) && fs.statSync(file).size + Buffer.byteLength(text) > MAX_BYTES) {
|
|
50
|
+
const retained = parseRows(file).slice(-500);
|
|
51
|
+
fs.writeFileSync(file, retained.map((item) => JSON.stringify({ v: 1, op: 'failed', ...item })).join('\n') + (retained.length ? '\n' : ''), { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
fs.appendFileSync(file, text, { mode: 0o600 });
|
|
54
|
+
}
|
package/dist/notifications.js
CHANGED
|
@@ -2,56 +2,40 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { loadSquare } from './artifact.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isDeliveryDelivered, isPendingNotification, planActNotifications, } from './delivery.js';
|
|
6
|
+
import { recordNotificationFailure } from './notification-failures.js';
|
|
6
7
|
import { hasPresentedAttention } from './presented.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { defaultWakeSinks
|
|
10
|
-
export { planActNotifications }
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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) {
|
|
8
|
+
import { SquareError } from './model.js';
|
|
9
|
+
import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
10
|
+
import { defaultWakeSinks } from './wake-sink.js';
|
|
11
|
+
export { planActNotifications, matchesMentionTarget };
|
|
12
|
+
function known(doc, name) {
|
|
13
|
+
const value = resolveRosterName(doc, name);
|
|
14
|
+
if (value === undefined)
|
|
25
15
|
throw new SquareError('invalid_args', `Unknown participant "${name}". Expected one of: ${rosterNames(doc).join(', ')}.`);
|
|
26
|
-
|
|
27
|
-
return known;
|
|
16
|
+
return value;
|
|
28
17
|
}
|
|
18
|
+
export { notificationMessageId } from './delivery.js';
|
|
29
19
|
export function notificationDeliveryWaitMs() {
|
|
30
|
-
|
|
20
|
+
const value = Number.parseInt(process.env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
|
|
21
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
22
|
+
throw new SquareError('invalid_args', 'Invalid SQUARE_NOTIFY_DELIVERY_WAIT_MS: expected a positive integer.');
|
|
23
|
+
return value;
|
|
31
24
|
}
|
|
32
|
-
export function
|
|
25
|
+
export function hasDeliveredNotification(squarePath, name, ref) {
|
|
33
26
|
const doc = loadSquare(squarePath);
|
|
34
|
-
|
|
35
|
-
const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
|
|
36
|
-
return isDeliveryDelivered(doc, known, index);
|
|
27
|
+
return isDeliveryDelivered(doc, known(doc, name), typeof ref === 'number' ? ref : Number(ref.slice(4)));
|
|
37
28
|
}
|
|
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) {
|
|
29
|
+
export function hasAttentionNotification(squarePath, name, ref, env = process.env) {
|
|
43
30
|
const doc = loadSquare(squarePath);
|
|
44
|
-
const
|
|
31
|
+
const recipient = known(doc, name);
|
|
45
32
|
const index = typeof ref === 'number' ? ref : Number(ref.slice(4));
|
|
46
|
-
|
|
47
|
-
return true;
|
|
48
|
-
return hasPresentedAttention(squarePath, known, index, env);
|
|
33
|
+
return isDeliveryDelivered(doc, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
|
|
49
34
|
}
|
|
50
|
-
export async function
|
|
51
|
-
const
|
|
52
|
-
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
export async function waitForDeliveredNotification(squarePath, name, ref, opts = {}) {
|
|
36
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30000);
|
|
53
37
|
while (Date.now() <= deadline) {
|
|
54
|
-
if (
|
|
38
|
+
if (hasDeliveredNotification(squarePath, name, ref))
|
|
55
39
|
return true;
|
|
56
40
|
await sleep(Math.min(SLEEP_MS, Math.max(1, deadline - Date.now())));
|
|
57
41
|
}
|
|
@@ -59,39 +43,40 @@ export async function waitForDeliveredMention(squarePath, name, ref, opts = {})
|
|
|
59
43
|
}
|
|
60
44
|
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
61
45
|
const doc = loadSquare(squarePath);
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
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)
|
|
46
|
+
const item = doc.acts.find((candidate) => candidate.index === actIndex);
|
|
47
|
+
if (item === undefined)
|
|
69
48
|
return;
|
|
49
|
+
const notifications = planActNotifications(doc, item).filter(isPendingNotification);
|
|
70
50
|
for (const notification of notifications) {
|
|
71
|
-
|
|
72
|
-
if (hasAttentionMention(squarePath, notification.recipient, actIndex))
|
|
51
|
+
if (hasAttentionNotification(squarePath, notification.recipient, notification.item.index))
|
|
73
52
|
continue;
|
|
74
|
-
for (const sink of sinks) {
|
|
75
|
-
|
|
53
|
+
for (const sink of opts.sinks ?? defaultWakeSinks()) {
|
|
54
|
+
try {
|
|
55
|
+
await sink.dispatch(notification, { squarePath });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
recordNotificationFailure(squarePath, {
|
|
59
|
+
actIndex: notification.item.index,
|
|
60
|
+
recipient: notification.recipient,
|
|
61
|
+
route: notification.route,
|
|
62
|
+
sink: sink.name,
|
|
63
|
+
message: error instanceof Error ? error.message : String(error),
|
|
64
|
+
...(error instanceof Error && 'diagnostic' in error ? { diagnostic: error.diagnostic } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
76
67
|
}
|
|
77
68
|
}
|
|
78
69
|
}
|
|
79
|
-
function
|
|
80
|
-
const child = spawn(process.execPath, [workerPath, ...args], {
|
|
81
|
-
detached: true,
|
|
82
|
-
stdio: 'ignore',
|
|
83
|
-
env: process.env,
|
|
84
|
-
});
|
|
70
|
+
function launchWorker(workerPath, args) {
|
|
71
|
+
const child = spawn(process.execPath, [workerPath, ...args], { detached: true, stdio: 'ignore', env: process.env });
|
|
85
72
|
child.unref();
|
|
86
73
|
}
|
|
74
|
+
/** Start one detached worker only when this act contains directed attention. */
|
|
87
75
|
export async function dispatchActNotifications(squarePath, item, opts = {}) {
|
|
88
|
-
if (process.env
|
|
76
|
+
if (process.env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
89
77
|
return;
|
|
90
78
|
const doc = loadSquare(squarePath);
|
|
91
|
-
|
|
92
|
-
if (notifications.length === 0)
|
|
79
|
+
if (!planActNotifications(doc, item).some(isPendingNotification))
|
|
93
80
|
return;
|
|
94
|
-
|
|
95
|
-
const launch = opts.launchWorker ?? launchDetachedWorker;
|
|
96
|
-
launch(workerPath, ['--square-path', squarePath, '--act-index', String(item.index)]);
|
|
81
|
+
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--square-path', squarePath, '--act-index', String(item.index)]);
|
|
97
82
|
}
|