@astrosheep/square 0.3.57 → 0.3.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +16 -14
- package/dist/attention-presentation.js +19 -19
- package/dist/automatic-session.js +2 -2
- package/dist/catch-decisions.js +1 -1
- package/dist/cli/context.js +16 -16
- package/dist/cli/observation-commands.js +85 -45
- package/dist/cli/program.js +33 -9
- package/dist/cli/square-commands.js +21 -19
- package/dist/decisions.d.ts +2 -0
- package/dist/decisions.js +38 -31
- package/dist/delivery-health.d.ts +3 -2
- package/dist/delivery-health.js +79 -47
- package/dist/delivery-operations.js +50 -8
- package/dist/diagnostic-redaction.d.ts +2 -0
- package/dist/diagnostic-redaction.js +40 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.js +2 -2
- package/dist/host-ledger.d.ts +9 -2
- package/dist/list.js +12 -9
- package/dist/notifications.js +28 -11
- package/dist/ports.d.ts +5 -1
- package/dist/presentation.d.ts +7 -0
- package/dist/presentation.js +75 -67
- package/dist/registry.js +2 -2
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +37 -0
- package/dist/square-actions.js +4 -4
- package/dist/stream.js +1 -1
- package/dist/tty-style.d.ts +7 -0
- package/dist/tty-style.js +13 -0
- package/dist/views.js +6 -5
- package/dist/wake-attempts.d.ts +18 -0
- package/dist/wake-attempts.js +38 -11
- package/dist/watch.js +24 -11
- package/extensions/square-pi.js +16 -6
- package/package.json +3 -3
package/dist/activity.js
CHANGED
|
@@ -3,14 +3,16 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
5
|
import { SquareError, isSquareError, validateName } from './model.js';
|
|
6
|
-
import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, withPathOutput, } from './presentation.js';
|
|
7
|
-
import { nowMs, SLEEP_MS } from './runtime.js';
|
|
6
|
+
import { expressHintLine, formatRefusal, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, withPathOutput, } from './presentation.js';
|
|
7
|
+
import { nowMs, inSquareCount, SLEEP_MS } from './runtime.js';
|
|
8
|
+
import { unreadActivitySummaries } from './decisions.js';
|
|
8
9
|
import { openSquare } from './square-file-adapter.js';
|
|
9
10
|
import { closeOpenSquare } from './open-square.js';
|
|
10
11
|
import { Square } from './square-wiring.js';
|
|
11
12
|
import { activityPresentation, resolveParticipant } from './views.js';
|
|
12
13
|
import { formatActivityId } from './square-core.js';
|
|
13
|
-
import {
|
|
14
|
+
import { formatDuration } from './time.js';
|
|
15
|
+
import { style } from './tty-style.js';
|
|
14
16
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
15
17
|
import { createDefaultWakeTransport } from './notifications.js';
|
|
16
18
|
function draftDirFor(squarePath) {
|
|
@@ -49,14 +51,15 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
49
51
|
knownName = (await resolveParticipant(reader, name)).name;
|
|
50
52
|
}
|
|
51
53
|
catch (err) {
|
|
54
|
+
const participantCount = inSquareCount((await reader.artifact.read()).state);
|
|
52
55
|
await closeOpenSquare(reader);
|
|
53
56
|
if (isSquareError(err)) {
|
|
54
57
|
if (err.code === 'unknown_participant' || err.code === 'invalid_args') {
|
|
55
58
|
const draftPath = saveActivityDraft(squarePath, name, rawInput);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
const bodyLines = [err.message, `· draft kept: ${draftPath}`];
|
|
60
|
+
if (/^✕ .* has never stepped into/.test(err.message))
|
|
61
|
+
bodyLines.push(joinRecoveryCommand(squarePath, name));
|
|
62
|
+
process.stderr.write(formatRefusal(squarePath, bodyLines, { participantCount }));
|
|
60
63
|
process.exit(2);
|
|
61
64
|
}
|
|
62
65
|
process.stderr.write(err.message + '\n');
|
|
@@ -81,7 +84,7 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
81
84
|
const pendingPublic = before.pendingPublic;
|
|
82
85
|
const pendingRoomChanges = before.pendingRoomChanges;
|
|
83
86
|
try {
|
|
84
|
-
await participant.express(body, {
|
|
87
|
+
const landed = await participant.express(body, {
|
|
85
88
|
force,
|
|
86
89
|
...(opts.mentions === undefined ? {} : { mentions: opts.mentions }),
|
|
87
90
|
...(reach === undefined ? {} : { reach }),
|
|
@@ -95,8 +98,8 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
95
98
|
const hasPending = pendingPublic.length > 0 || pendingRoomChanges.length > 0;
|
|
96
99
|
const pending = hasPending ? `\n\n${renderPendingFeed([...fresh.activities], [...pendingPublic], [...pendingRoomChanges], knownName, fresh.state)}` : '';
|
|
97
100
|
const hint = expressHintLine(ownActCount);
|
|
98
|
-
const confirmation = `●
|
|
99
|
-
const withHint = hint ? `${confirmation}\n${hint}` : confirmation;
|
|
101
|
+
const confirmation = `● your activity lands${style('dim', ` — #${ownActCount} · ${landed.activity.id}`)}`;
|
|
102
|
+
const withHint = hint ? `${confirmation}\n${style('dim', hint)}` : confirmation;
|
|
100
103
|
process.stdout.write(withPathOutput(squarePath, withHint + pending, { participantCount: headerCount, held }));
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
@@ -113,8 +116,8 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
113
116
|
squarePath,
|
|
114
117
|
name: knownName,
|
|
115
118
|
forceCommand: opts.forceCommand,
|
|
116
|
-
activitySummaries:
|
|
117
|
-
unreadRoomChanges: [
|
|
119
|
+
activitySummaries: unreadActivitySummaries(fresh.state, knownName, nowMs()),
|
|
120
|
+
unreadRoomChanges: [],
|
|
118
121
|
draftPath,
|
|
119
122
|
participantCount: headerCount,
|
|
120
123
|
held,
|
|
@@ -164,8 +167,7 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
164
167
|
continue;
|
|
165
168
|
}
|
|
166
169
|
if (error.code === 'bell_quota') {
|
|
167
|
-
|
|
168
|
-
process.stdout.write(withPathOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again at ${formatTimestamp(nextAt)}`].join('\n'), { participantCount: headerCount, held }));
|
|
170
|
+
process.stdout.write(withPathOutput(squarePath, [`✕ the bell stays quiet for now`, ` · you can ring it again in ${formatDuration(error.facts?.retryAfterMs ?? 1)}`].join('\n'), { participantCount: headerCount, held }));
|
|
169
171
|
process.exit(1);
|
|
170
172
|
return;
|
|
171
173
|
}
|
|
@@ -17,29 +17,29 @@ export function displayAttentionPath(squarePath) {
|
|
|
17
17
|
: squarePath;
|
|
18
18
|
}
|
|
19
19
|
export function renderAttentionPreview(attention) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const [lastKey, lastValue] = entries[entries.length - 1];
|
|
20
|
+
// A boundary preview only ever reaches the recipient's own session, so the
|
|
21
|
+
// recipient is always "you"; the name follows in parens for recognition.
|
|
22
|
+
const route = attention.route === 'bell'
|
|
23
|
+
? `${attention.actor} rang the bell for you (${attention.recipient})`
|
|
24
|
+
: `${attention.actor} called your name (${attention.recipient})`;
|
|
25
|
+
// The fence is what keeps the body from being re-rendered by Markdown. It grows past any
|
|
26
|
+
// backtick run in the body so a body containing a fence cannot break out of this one.
|
|
27
|
+
// Everything outside the fence is the system's voice, which participant bodies cannot fake.
|
|
28
|
+
const fence = '`'.repeat(Math.max(3, longestBacktickRun(attention.body) + 1));
|
|
30
29
|
return [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
`${fence}square-activity`,
|
|
31
|
+
`· ${displayAttentionPath(attention.squarePath)} · ${formatActivityId(attention.actIndex)}`,
|
|
32
|
+
`● ${route}`,
|
|
33
|
+
'',
|
|
35
34
|
previewAttentionBody(attention.body),
|
|
36
|
-
|
|
35
|
+
fence,
|
|
36
|
+
// A clipped body ends with the full command to read it all: a labeled caption, then the
|
|
37
|
+
// command bare on its own line so it can be copied and run as it is.
|
|
37
38
|
...(attentionBodyIsClipped(attention.body)
|
|
38
|
-
? [
|
|
39
|
+
? ['· clipped — read it all:', `${participantCommandPrefix(attention.squarePath, attention.recipient)} catch --id ${formatActivityId(attention.actIndex)}`]
|
|
39
40
|
: []),
|
|
40
41
|
].join('\n');
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
-
return value.
|
|
44
|
-
.replace(/\r/g, ' ').replace(/\n/g, ' ').replace(/\t/g, '	');
|
|
43
|
+
function longestBacktickRun(value) {
|
|
44
|
+
return value.match(/`+/g)?.reduce((longest, run) => Math.max(longest, run.length), 0) ?? 0;
|
|
45
45
|
}
|
|
@@ -71,10 +71,10 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
71
71
|
await closeOpenSquare(reader);
|
|
72
72
|
}
|
|
73
73
|
if (bindings.some((binding) => binding.session !== sessionId)) {
|
|
74
|
-
throw new SquareError('already_joined',
|
|
74
|
+
throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
|
|
75
75
|
}
|
|
76
76
|
if (entry.joined && !bindings.some((binding) => binding.session === sessionId)) {
|
|
77
|
-
throw new SquareError('already_joined',
|
|
77
|
+
throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
|
|
78
78
|
}
|
|
79
79
|
const scopedEnv = operationEnv(provider, sessionId, env);
|
|
80
80
|
const claim = await claimSessionParticipant(squarePath, name, scopedEnv);
|
package/dist/catch-decisions.js
CHANGED
|
@@ -17,7 +17,7 @@ function resolveCatchName(state, requestedName) {
|
|
|
17
17
|
validateName(requestedName);
|
|
18
18
|
const known = resolveRosterName(state, requestedName);
|
|
19
19
|
if (known === undefined) {
|
|
20
|
-
throw new SquareError('invalid_args',
|
|
20
|
+
throw new SquareError('invalid_args', `✕ ${participantIdentity(requestedName)} has never stepped into this square`);
|
|
21
21
|
}
|
|
22
22
|
return known;
|
|
23
23
|
}
|
package/dist/cli/context.js
CHANGED
|
@@ -35,66 +35,66 @@ export function usage(command) {
|
|
|
35
35
|
export function requireValue(args, index, flag) {
|
|
36
36
|
const value = args[index + 1];
|
|
37
37
|
if (value === undefined || value.startsWith('--'))
|
|
38
|
-
fail(
|
|
38
|
+
fail(`✕ missing value for ${flag}`);
|
|
39
39
|
return value;
|
|
40
40
|
}
|
|
41
41
|
export function parseDurationMs(value, flag) {
|
|
42
42
|
const match = value.match(/^([1-9]\d*)(ms|s|m|h)$/);
|
|
43
43
|
if (!match)
|
|
44
|
-
fail(
|
|
44
|
+
fail(`✕ invalid ${flag}: expected a positive duration with unit ms, s, m, or h (for example 500ms, 30s, 3m, 1h)`);
|
|
45
45
|
const amount = Number.parseInt(match[1], 10);
|
|
46
46
|
const multiplier = { ms: 1, s: 1000, m: 60000, h: 3600000 };
|
|
47
47
|
const milliseconds = amount * multiplier[match[2]];
|
|
48
48
|
if (!Number.isSafeInteger(milliseconds))
|
|
49
|
-
fail(
|
|
49
|
+
fail(`✕ invalid ${flag}: duration is too large`);
|
|
50
50
|
return milliseconds;
|
|
51
51
|
}
|
|
52
52
|
export function parsePositiveInteger(value, flag) {
|
|
53
53
|
if (!/^[1-9]\d*$/.test(value))
|
|
54
|
-
fail(
|
|
54
|
+
fail(`✕ invalid ${flag}: expected a positive integer`);
|
|
55
55
|
const parsed = Number(value);
|
|
56
56
|
if (!Number.isSafeInteger(parsed))
|
|
57
|
-
fail(
|
|
57
|
+
fail(`✕ invalid ${flag}: value is too large`);
|
|
58
58
|
return parsed;
|
|
59
59
|
}
|
|
60
60
|
export function parseBoundedLimit(value, flag, max, retry) {
|
|
61
61
|
if (value === undefined || value.startsWith('--') || !/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
|
|
62
|
-
fail(`✕ ${flag} needs a positive integer\n
|
|
62
|
+
fail(`✕ ${flag} needs a positive integer\n${retry}`);
|
|
63
63
|
}
|
|
64
64
|
const parsed = Number(value);
|
|
65
65
|
if (parsed > max)
|
|
66
|
-
fail(`✕ ${flag} is capped at ${max}\n
|
|
66
|
+
fail(`✕ ${flag} is capped at ${max}\n${retry}`);
|
|
67
67
|
return parsed;
|
|
68
68
|
}
|
|
69
69
|
export function parseNonNegativeInteger(value, flag) {
|
|
70
70
|
if (!/^\d+$/.test(value))
|
|
71
|
-
fail(
|
|
71
|
+
fail(`✕ invalid ${flag}: expected a non-negative integer`);
|
|
72
72
|
const parsed = Number(value);
|
|
73
73
|
if (!Number.isSafeInteger(parsed))
|
|
74
|
-
fail(
|
|
74
|
+
fail(`✕ invalid ${flag}: value is too large`);
|
|
75
75
|
return parsed;
|
|
76
76
|
}
|
|
77
77
|
export function parseHardCap(value) {
|
|
78
78
|
if (value === 'unlimited')
|
|
79
79
|
return null;
|
|
80
80
|
if (!/^[1-9]\d*$/.test(value))
|
|
81
|
-
fail('
|
|
81
|
+
fail('✕ invalid build option: --cap must be a positive integer or unlimited');
|
|
82
82
|
const parsed = Number(value);
|
|
83
83
|
if (!Number.isSafeInteger(parsed))
|
|
84
|
-
fail('
|
|
84
|
+
fail('✕ invalid build option: --cap must be a positive integer or unlimited');
|
|
85
85
|
return parsed;
|
|
86
86
|
}
|
|
87
87
|
export function parseNameList(value, flag) {
|
|
88
88
|
const names = parseParticipantList(value);
|
|
89
89
|
if (names.length === 0)
|
|
90
|
-
fail(
|
|
90
|
+
fail(`✕ invalid ${flag}: expected at least one participant name`);
|
|
91
91
|
for (const name of names)
|
|
92
92
|
validateName(name);
|
|
93
93
|
return names;
|
|
94
94
|
}
|
|
95
95
|
export function requireParticipant(name) {
|
|
96
96
|
if (!name)
|
|
97
|
-
fail('
|
|
97
|
+
fail('✕ missing required option: --as <name>');
|
|
98
98
|
validateName(name);
|
|
99
99
|
return name;
|
|
100
100
|
}
|
|
@@ -157,7 +157,7 @@ export async function parseGlobalArgs(rawArgs) {
|
|
|
157
157
|
}
|
|
158
158
|
const command = args[0];
|
|
159
159
|
if (command === 'history' && hasExplicitName) {
|
|
160
|
-
fail(`✕ history is an archive and does not use --as\n
|
|
160
|
+
fail(`✕ history is an archive and does not use --as\n${withoutAsCommand(rawArgs)}`);
|
|
161
161
|
}
|
|
162
162
|
if (name === undefined)
|
|
163
163
|
name = configuredName();
|
|
@@ -166,7 +166,7 @@ export async function parseGlobalArgs(rawArgs) {
|
|
|
166
166
|
const explicitSquarePath = requestedPath !== undefined;
|
|
167
167
|
const configured = configuredLocation();
|
|
168
168
|
if (command !== undefined && !['--help', '-h'].includes(command) && locationIsRequired(command) && requestedPath === undefined && configured === undefined) {
|
|
169
|
-
fail(`✕ ${command} needs a square location\
|
|
169
|
+
fail(`✕ ${command} needs a square location\nsquare ls`);
|
|
170
170
|
}
|
|
171
171
|
const squarePath = requestedPath ?? configured;
|
|
172
172
|
if (name === undefined && squarePath !== undefined && command !== undefined && locationIsRequired(command)) {
|
|
@@ -189,6 +189,6 @@ export function defaultContext(command, squarePath, name) {
|
|
|
189
189
|
}
|
|
190
190
|
export function requireSquarePath(context) {
|
|
191
191
|
if (context.squarePath === undefined)
|
|
192
|
-
fail(`✕ ${context.command} needs a square location\
|
|
192
|
+
fail(`✕ ${context.command} needs a square location\nsquare ls`);
|
|
193
193
|
return context.squarePath;
|
|
194
194
|
}
|
|
@@ -6,6 +6,7 @@ import { cmdListSquares } from '../list.js';
|
|
|
6
6
|
import { nameKey, parseActivityId, sameName } from '../model.js';
|
|
7
7
|
import { commandPrefix, participantIdentity, renderGrepActivitiesView, renderEventCli, renderAmbientEvent, renderPresenceAnchor, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
|
|
8
8
|
import { actId, nowMs } from '../runtime.js';
|
|
9
|
+
import { style } from '../tty-style.js';
|
|
9
10
|
import { cmdStream, cmdStreamNdjson } from '../stream.js';
|
|
10
11
|
import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
|
|
11
12
|
import { cmdWatch } from '../watch.js';
|
|
@@ -15,6 +16,8 @@ import { historyPresentation, participantsPresentation, statusPresentation } fro
|
|
|
15
16
|
import { fail, parseBoundedLimit, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
|
|
16
17
|
import { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from '../catch-decisions.js';
|
|
17
18
|
const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
|
|
19
|
+
/** A participant counts as here when their last sign of life sits inside this window. */
|
|
20
|
+
const STATUS_HERE_WINDOW_MS = 30 * 60_000;
|
|
18
21
|
const HISTORY_DEFAULT_LIMIT = 10;
|
|
19
22
|
const HISTORY_MAX_LIMIT = 100;
|
|
20
23
|
const PARTICIPANTS_DEFAULT_LIMIT = 20;
|
|
@@ -94,10 +97,10 @@ export const catchCommand = {
|
|
|
94
97
|
for (let index = 0; index < argv.length; index++) {
|
|
95
98
|
if (argv[index] === '--id') {
|
|
96
99
|
if (id !== undefined)
|
|
97
|
-
fail('✕ catch accepts one --id\
|
|
100
|
+
fail('✕ catch accepts one --id\nsquare catch --help');
|
|
98
101
|
const value = requireValue(argv, index, '--id');
|
|
99
102
|
if (parseActivityId(value) === undefined)
|
|
100
|
-
fail('✕ invalid --id: expected an activity id like act/12\
|
|
103
|
+
fail('✕ invalid --id: expected an activity id like act/12\nsquare catch --help');
|
|
101
104
|
id = value;
|
|
102
105
|
index += 1;
|
|
103
106
|
}
|
|
@@ -121,11 +124,11 @@ export const catchCommand = {
|
|
|
121
124
|
else if (argv[index] === '--now')
|
|
122
125
|
now = true;
|
|
123
126
|
else
|
|
124
|
-
fail(`✕ catch does not know ${argv[index]}\
|
|
127
|
+
fail(`✕ catch does not know ${argv[index]}\nsquare catch --help`);
|
|
125
128
|
}
|
|
126
129
|
if (id !== undefined) {
|
|
127
130
|
if (argv.some((flag) => ['--idle', '--from', '--mention', '--limit', '--replace'].includes(flag))) {
|
|
128
|
-
fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\
|
|
131
|
+
fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\nsquare catch --help');
|
|
129
132
|
}
|
|
130
133
|
return { id, now: true };
|
|
131
134
|
}
|
|
@@ -271,7 +274,7 @@ function parseHistory(argv, context) {
|
|
|
271
274
|
else if (flag === '--json')
|
|
272
275
|
json = true;
|
|
273
276
|
else
|
|
274
|
-
fail(`✕ history does not know ${flag}\
|
|
277
|
+
fail(`✕ history does not know ${flag}\nsquare history --help`);
|
|
275
278
|
}
|
|
276
279
|
if (grep !== undefined && fixed !== undefined)
|
|
277
280
|
fail('--grep and --fixed cannot be combined.');
|
|
@@ -359,7 +362,7 @@ function renderHistoryProjection(projection, visible, noTruncate, squarePath) {
|
|
|
359
362
|
if (chunks.length === 0)
|
|
360
363
|
return 'latest\n ○ no public activity in this view';
|
|
361
364
|
if (preview !== undefined && shown.some((activity) => activity.kind === 'say' && activity.body.length > preview)) {
|
|
362
|
-
chunks.push(
|
|
365
|
+
chunks.push(`${commandPrefix(squarePath)} history --no-truncate`);
|
|
363
366
|
}
|
|
364
367
|
return chunks.join('\n\n');
|
|
365
368
|
}
|
|
@@ -374,7 +377,7 @@ export const historyCommand = {
|
|
|
374
377
|
const projection = await historyPresentation(square, { ...options, order: 'asc' });
|
|
375
378
|
let events = [...projection.activities];
|
|
376
379
|
if (options.lastN === null && events.length > HISTORY_MAX_LIMIT) {
|
|
377
|
-
fail(`✕ history is capped at ${HISTORY_MAX_LIMIT} activities\n
|
|
380
|
+
fail(`✕ history is capped at ${HISTORY_MAX_LIMIT} activities\n${boundedHistoryCommand(options, squarePath)}`);
|
|
378
381
|
}
|
|
379
382
|
const searching = options.grep !== undefined || options.fixed !== undefined;
|
|
380
383
|
const totalMatches = searching ? events.length : 0;
|
|
@@ -403,7 +406,7 @@ export const historyCommand = {
|
|
|
403
406
|
: allPublic.some((item) => item.index < (pageMin ?? Infinity)));
|
|
404
407
|
const cursorDirection = options.afterIndex !== undefined ? '--after' : '--before';
|
|
405
408
|
const cursorIndex = cursorDirection === '--after' ? Math.max(...publicEvents.map((item) => item.index)) : Math.min(...publicEvents.map((item) => item.index));
|
|
406
|
-
const continuation = hasMore ? `\n\n
|
|
409
|
+
const continuation = hasMore ? `\n\n${historyContinuationCommand(options, squarePath, cursorDirection, cursorIndex)}` : '';
|
|
407
410
|
return withPathOutput(squarePath, output + continuation, { participantCount: projection.participantCount });
|
|
408
411
|
}
|
|
409
412
|
finally {
|
|
@@ -434,18 +437,19 @@ export const participantsCommand = {
|
|
|
434
437
|
const now = nowMs();
|
|
435
438
|
const participants = await participantsPresentation(square);
|
|
436
439
|
const lines = participants.slice(0, intent.limit).map((participant) => {
|
|
437
|
-
const
|
|
440
|
+
const recent = participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS;
|
|
441
|
+
const glyph = participant.state === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && recent ? '●' : '○';
|
|
438
442
|
const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
|
|
439
443
|
const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
|
|
440
|
-
return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`;
|
|
444
|
+
return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
|
|
441
445
|
});
|
|
442
446
|
const participantCount = participants.filter((participant) => participant.state === 'active').length;
|
|
443
447
|
const tail = participants.length <= intent.limit
|
|
444
448
|
? []
|
|
445
449
|
: [
|
|
446
|
-
` ○ ${lines.length} of ${participants.length} participants shown
|
|
450
|
+
style('dim', ` ○ ${lines.length} of ${participants.length} participants shown`),
|
|
447
451
|
...(participants.length <= PARTICIPANTS_MAX_LIMIT ? [
|
|
448
|
-
|
|
452
|
+
`${participantsLimitCommand(squarePath, participants.length)}`,
|
|
449
453
|
] : []),
|
|
450
454
|
];
|
|
451
455
|
return withPathOutput(squarePath, ['participants', ...lines, ...tail].join('\n'), {
|
|
@@ -468,36 +472,55 @@ export const statusCommand = {
|
|
|
468
472
|
const presentation = await statusPresentation(square);
|
|
469
473
|
const result = presentation.status;
|
|
470
474
|
const active = result.participants.filter((participant) => participant.state === 'active');
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const attention = !showAttention
|
|
482
|
-
? ''
|
|
483
|
-
: participant.pendingMentionCount > 0
|
|
484
|
-
? `${participant.pendingMentionCount} attention${participant.pendingMentionCount === 1 ? '' : 's'} waiting`
|
|
485
|
-
: participant.unreadActivityCount > 0
|
|
486
|
-
? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
|
|
487
|
-
: 'caught up';
|
|
488
|
-
return ` ${glyph} ${participantIdentity(participant.name)} · ${summary}${attention === '' ? '' : ` · ${attention}`}`;
|
|
489
|
-
});
|
|
490
|
-
if (active.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
|
|
491
|
-
people.push(` ○ … ${active.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more participants`);
|
|
492
|
-
people.push(`» ${result.participants.length <= PARTICIPANTS_MAX_LIMIT
|
|
493
|
-
? participantsLimitCommand(squarePath, result.participants.length)
|
|
494
|
-
: `${commandPrefix(squarePath)} participants`}`);
|
|
475
|
+
const here = active.filter((participant) => participant.presence === 'watching'
|
|
476
|
+
|| (participant.lastActiveAt !== undefined && result.now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS));
|
|
477
|
+
const lingering = active.length - here.length;
|
|
478
|
+
const orderedHere = [
|
|
479
|
+
...here.filter((participant) => participant.presence === 'watching'),
|
|
480
|
+
...here.filter((participant) => participant.presence !== 'watching'),
|
|
481
|
+
];
|
|
482
|
+
const people = [];
|
|
483
|
+
if (active.length === 0) {
|
|
484
|
+
people.push(' ○ nobody in the square');
|
|
495
485
|
}
|
|
496
|
-
|
|
486
|
+
else {
|
|
487
|
+
if (orderedHere.length === 0)
|
|
488
|
+
people.push(' ○ nobody here right now');
|
|
489
|
+
people.push(...orderedHere.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
|
|
490
|
+
const glyph = participant.presence === 'watching'
|
|
491
|
+
? '◎'
|
|
492
|
+
: participant.activityCount > 0 ? '●' : '○';
|
|
493
|
+
const summary = participant.presence === 'watching'
|
|
494
|
+
? 'catching'
|
|
495
|
+
: participant.activityCount > 0
|
|
496
|
+
? formatRelativeTime(participant.lastActiveAt ?? result.now, result.now)
|
|
497
|
+
: `quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
|
|
498
|
+
const showAttention = context.name === undefined || sameName(participant.name, context.name);
|
|
499
|
+
const attention = !showAttention
|
|
500
|
+
? ''
|
|
501
|
+
: participant.pendingMentionCount > 0
|
|
502
|
+
? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
|
|
503
|
+
: participant.unreadActivityCount > 0
|
|
504
|
+
? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
|
|
505
|
+
: 'caught up';
|
|
506
|
+
return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` · ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
|
|
507
|
+
}));
|
|
508
|
+
const unshown = Math.max(0, orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT) + lingering;
|
|
509
|
+
if (orderedHere.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
|
|
510
|
+
people.push(` ○ … ${orderedHere.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more here`);
|
|
511
|
+
}
|
|
512
|
+
if (lingering > 0)
|
|
513
|
+
people.push(` ○ … ${lingering} lingering`);
|
|
514
|
+
if (unshown > 0) {
|
|
515
|
+
people.push(`${result.participants.length <= PARTICIPANTS_MAX_LIMIT
|
|
516
|
+
? participantsLimitCommand(squarePath, result.participants.length)
|
|
517
|
+
: `${commandPrefix(squarePath)} participants`}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const cap = result.hardCap === null ? 'unlimited' : `${result.hardCap} each`;
|
|
521
|
+
const holdTime = result.holdAt === undefined ? 'just now' : formatRelativeTime(result.holdAt, result.now);
|
|
497
522
|
const hold = result.holdActive
|
|
498
|
-
? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''} · ${
|
|
499
|
-
? 'just now'
|
|
500
|
-
: formatRelativeTime(result.holdAt, result.now)}`
|
|
523
|
+
? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''}${style('dim', ` · ${holdTime}`)}`
|
|
501
524
|
: undefined;
|
|
502
525
|
const visible = result.latestAct === undefined
|
|
503
526
|
? ''
|
|
@@ -510,14 +533,31 @@ export const statusCommand = {
|
|
|
510
533
|
const latest = visible === ''
|
|
511
534
|
? [result.latestAct === undefined
|
|
512
535
|
? ' ○ no public activity yet'
|
|
513
|
-
: ' · latest
|
|
536
|
+
: ' · the latest words were meant for other ears']
|
|
514
537
|
: [` ${visible.replace(/\n/g, '\n ')}`];
|
|
538
|
+
// Presence events after the latest public act: otherwise the roster can say someone is
|
|
539
|
+
// here while the latest line shows them leaving, with nothing to reconcile the two.
|
|
540
|
+
const latestIndex = result.latestAct?.index ?? -1;
|
|
541
|
+
const stepsIn = presentation.state.acts.filter((act) => act.kind === 'join' && act.index > latestIndex);
|
|
542
|
+
for (const act of stepsIn.slice(-3)) {
|
|
543
|
+
const rejoined = presentation.state.acts.some((other) => other.kind === 'done' && other.actor === act.actor && other.index < act.index);
|
|
544
|
+
latest.push(style('dim', ` → ${participantIdentity(act.actor)} ${rejoined ? 'stepped back in' : 'stepped in'} · ${formatRelativeTime(act.at, result.now)}`));
|
|
545
|
+
}
|
|
546
|
+
if (stepsIn.length > 3)
|
|
547
|
+
latest.push(style('dim', ` → … ${stepsIn.length - 3} more stepped in`));
|
|
515
548
|
if (visible.includes('more chars') && result.latestAct !== undefined) {
|
|
516
|
-
latest.push(
|
|
549
|
+
latest.push(`${commandPrefix(squarePath)} history --at ${actId(result.latestAct)} -C 2 --no-truncate`);
|
|
517
550
|
}
|
|
551
|
+
const counts = [
|
|
552
|
+
`${here.length} here`,
|
|
553
|
+
...(lingering > 0 ? [`${lingering} lingering`] : []),
|
|
554
|
+
`${result.doneCount} done`,
|
|
555
|
+
`cap ${cap}`,
|
|
556
|
+
`throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
|
|
557
|
+
].join(' · ');
|
|
518
558
|
const output = [
|
|
519
|
-
|
|
520
|
-
...(hold === undefined ? [] : ['', hold]), '', 'around the square', ...people, '', 'latest', ...latest,
|
|
559
|
+
style('dim', counts),
|
|
560
|
+
...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
|
|
521
561
|
].join('\n');
|
|
522
562
|
return withPathOutput(squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
|
|
523
563
|
}
|
|
@@ -558,7 +598,7 @@ export const inboxCommand = {
|
|
|
558
598
|
fail('inbox requires --for-session <session-id>.');
|
|
559
599
|
const retry = inboxLimitCommand(sessionId, json);
|
|
560
600
|
if (duplicateLimit)
|
|
561
|
-
fail(`✕ inbox accepts one --limit\n
|
|
601
|
+
fail(`✕ inbox accepts one --limit\n${retry}`);
|
|
562
602
|
const limit = hasLimit
|
|
563
603
|
? parseBoundedLimit(limitValue, '--limit', INBOX_MAX_LIMIT, retry)
|
|
564
604
|
: INBOX_DEFAULT_LIMIT;
|
package/dist/cli/program.js
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { helpRequest } from '../help.js';
|
|
2
2
|
import { isSquareError } from '../model.js';
|
|
3
|
-
import { joinRecoveryCommand, participantsRecoveryCommand } from '../presentation.js';
|
|
3
|
+
import { formatRefusal, joinRecoveryCommand, participantsRecoveryCommand } from '../presentation.js';
|
|
4
|
+
import { inSquareCount, nowMs } from '../runtime.js';
|
|
5
|
+
import { openSquare } from '../square-file-adapter.js';
|
|
6
|
+
import { closeOpenSquare } from '../open-square.js';
|
|
4
7
|
import { defaultContext, parseGlobalArgs } from './context.js';
|
|
5
8
|
import { executeRegisteredCommand, findCommand } from './registry.js';
|
|
6
|
-
function
|
|
9
|
+
async function participantCountFor(squarePath) {
|
|
10
|
+
try {
|
|
11
|
+
const square = await openSquare(squarePath, { clock: nowMs });
|
|
12
|
+
try {
|
|
13
|
+
return inSquareCount((await square.artifact.read()).state);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await closeOpenSquare(square);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function handleSquareError(error, squarePath, name) {
|
|
7
24
|
if (isSquareError(error)) {
|
|
8
|
-
|
|
9
|
-
if (squarePath !== undefined &&
|
|
25
|
+
const existing = [error.message];
|
|
26
|
+
if (squarePath !== undefined && /^✕ .* has never stepped into/.test(error.message)) {
|
|
10
27
|
// The caller's name has never joined this square; only a join admits it.
|
|
11
28
|
if (name !== undefined)
|
|
12
|
-
|
|
29
|
+
existing.push(joinRecoveryCommand(squarePath, name));
|
|
13
30
|
else
|
|
14
|
-
|
|
31
|
+
existing.push(participantsRecoveryCommand(squarePath));
|
|
32
|
+
}
|
|
33
|
+
else if (squarePath !== undefined && /^✕ .* is not standing in this square/.test(error.message)) {
|
|
34
|
+
existing.push(participantsRecoveryCommand(squarePath));
|
|
35
|
+
}
|
|
36
|
+
if (squarePath === undefined) {
|
|
37
|
+
process.stderr.write(`${existing.join('\n')}\n`);
|
|
15
38
|
}
|
|
16
|
-
else
|
|
17
|
-
|
|
39
|
+
else {
|
|
40
|
+
const participantCount = await participantCountFor(squarePath);
|
|
41
|
+
process.stderr.write(formatRefusal(squarePath, existing, participantCount === undefined ? {} : { participantCount }));
|
|
18
42
|
}
|
|
19
43
|
process.exit(error.code === 'not_found' ? 1 : 2);
|
|
20
44
|
}
|
|
@@ -45,6 +69,6 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
45
69
|
await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
|
|
46
70
|
}
|
|
47
71
|
catch (error) {
|
|
48
|
-
handleSquareError(error, squarePath, requestedName);
|
|
72
|
+
await handleSquareError(error, squarePath, requestedName);
|
|
49
73
|
}
|
|
50
74
|
}
|