@astrosheep/square 0.3.3 → 0.3.5
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 +1 -1
- package/dist/activity.js +15 -13
- package/dist/cli/context.js +143 -0
- package/dist/cli/harness-command.js +50 -0
- package/dist/cli/maintenance-commands.js +92 -0
- package/dist/cli/meta-commands.js +31 -0
- package/dist/cli/observation-commands.js +461 -0
- package/dist/cli/program.js +48 -0
- package/dist/cli/registry.js +40 -0
- package/dist/cli/square-commands.js +221 -0
- package/dist/compact.js +5 -18
- package/dist/harness-claude.js +275 -0
- package/dist/harness-codex.js +653 -0
- package/dist/harness-lifecycle.js +102 -0
- package/dist/harness-links.js +123 -0
- package/dist/harness.js +97 -572
- package/dist/help.js +2 -1
- package/dist/identity.js +27 -0
- package/dist/index.js +45 -32
- package/dist/runtime.js +0 -54
- package/dist/square-application.js +259 -0
- package/dist/square-store.js +111 -0
- package/dist/square.js +5 -1362
- package/dist/watch.js +17 -19
- package/package.json +3 -1
- package/skills/square/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { loadSquare } from '../artifact.js';
|
|
2
|
+
import { runClaudeHook, runCodexHook } from '../claude-hook.js';
|
|
3
|
+
import { coreActivities, coreParticipants, coreStatus } from '../decisions.js';
|
|
4
|
+
import { sessionInbox } from '../inbox.js';
|
|
5
|
+
import { cmdListSquares } from '../list.js';
|
|
6
|
+
import { sameName } from '../model.js';
|
|
7
|
+
import { commandPrefix, participantCommandPrefix, renderActivitiesView, renderGrepActivitiesView, renderVisibleEvent, withPathOutput, } from '../presentation.js';
|
|
8
|
+
import { recordLocalJoin } from '../registry.js';
|
|
9
|
+
import { actId, inSquareCount, isCurrentlyJoined, nowMs, resolveRosterName, sayNumberFor, } from '../runtime.js';
|
|
10
|
+
import { cmdStream, cmdStreamNdjson } from '../stream.js';
|
|
11
|
+
import { formatRelativeTime, formatTimestamp, parseTimeOrRelative } from '../time.js';
|
|
12
|
+
import { cmdWatch } from '../watch.js';
|
|
13
|
+
import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, parsePositiveInteger, readStdinSync, requireParticipant, requireValue, usage, } from './context.js';
|
|
14
|
+
export const listCommand = {
|
|
15
|
+
parse: (argv) => argv,
|
|
16
|
+
execute(argv, context) {
|
|
17
|
+
cmdListSquares(argv, () => usage(context.command));
|
|
18
|
+
},
|
|
19
|
+
present: () => { },
|
|
20
|
+
};
|
|
21
|
+
export const streamCommand = {
|
|
22
|
+
parse(argv, context) {
|
|
23
|
+
let ndjson = false;
|
|
24
|
+
let forName;
|
|
25
|
+
for (let index = 0; index < argv.length; index++) {
|
|
26
|
+
if (argv[index] === '--ndjson')
|
|
27
|
+
ndjson = true;
|
|
28
|
+
else if (argv[index] === '--for') {
|
|
29
|
+
forName = requireValue(argv, index, argv[index]);
|
|
30
|
+
index += 1;
|
|
31
|
+
}
|
|
32
|
+
else
|
|
33
|
+
usage(context.command);
|
|
34
|
+
}
|
|
35
|
+
if (forName !== undefined && !ndjson)
|
|
36
|
+
usage(context.command);
|
|
37
|
+
return { ndjson, forName };
|
|
38
|
+
},
|
|
39
|
+
async execute(intent, context) {
|
|
40
|
+
if (intent.ndjson)
|
|
41
|
+
await cmdStreamNdjson(context.squarePath, intent.forName);
|
|
42
|
+
else
|
|
43
|
+
await cmdStream(context.squarePath);
|
|
44
|
+
},
|
|
45
|
+
present: () => { },
|
|
46
|
+
};
|
|
47
|
+
export const catchCommand = {
|
|
48
|
+
parse(argv, context) {
|
|
49
|
+
const name = requireParticipant(context.name);
|
|
50
|
+
let activityCount = 1;
|
|
51
|
+
let idleMs;
|
|
52
|
+
let mention;
|
|
53
|
+
let force = false;
|
|
54
|
+
let now = false;
|
|
55
|
+
let follow = false;
|
|
56
|
+
const participants = [];
|
|
57
|
+
for (let index = 0; index < argv.length; index++) {
|
|
58
|
+
if (argv[index] === '--count') {
|
|
59
|
+
activityCount = parsePositiveInteger(requireValue(argv, index, argv[index]), argv[index]);
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
else if (argv[index] === '--by') {
|
|
63
|
+
participants.push(...parseNameList(requireValue(argv, index, argv[index]), argv[index]));
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
else if (argv[index] === '--idle') {
|
|
67
|
+
idleMs = parseDurationMs(requireValue(argv, index, argv[index]), argv[index]);
|
|
68
|
+
index += 1;
|
|
69
|
+
}
|
|
70
|
+
else if (argv[index] === '--mention') {
|
|
71
|
+
const value = argv[index + 1];
|
|
72
|
+
if (value !== undefined && !value.startsWith('--')) {
|
|
73
|
+
mention = value;
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
mention = name;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (argv[index] === '-f' || argv[index] === '--force')
|
|
81
|
+
force = true;
|
|
82
|
+
else if (argv[index] === '--now')
|
|
83
|
+
now = true;
|
|
84
|
+
else if (argv[index] === '--follow')
|
|
85
|
+
follow = true;
|
|
86
|
+
else
|
|
87
|
+
usage(context.command);
|
|
88
|
+
}
|
|
89
|
+
if (now && follow)
|
|
90
|
+
fail('--follow cannot be combined with --now');
|
|
91
|
+
return {
|
|
92
|
+
activityCount,
|
|
93
|
+
...(participants.length > 0 ? { participants } : {}),
|
|
94
|
+
...(mention === undefined ? {} : { mention }),
|
|
95
|
+
...(idleMs === undefined ? {} : { idleMs }),
|
|
96
|
+
...(force ? { force } : {}),
|
|
97
|
+
...(now ? { now } : {}),
|
|
98
|
+
...(follow ? { follow } : {}),
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
async execute(intent, context) {
|
|
102
|
+
await cmdWatch(context.squarePath, requireParticipant(context.name), intent);
|
|
103
|
+
},
|
|
104
|
+
present: () => { },
|
|
105
|
+
};
|
|
106
|
+
function parseActRef(value, flag) {
|
|
107
|
+
const match = value.trim().match(/^(?:act_)?(\d+)$/i);
|
|
108
|
+
if (!match)
|
|
109
|
+
fail(`Invalid ${flag}: expected act id like act_12 or 12.`);
|
|
110
|
+
return Number(match[1]);
|
|
111
|
+
}
|
|
112
|
+
function parseTimestamp(value, flag) {
|
|
113
|
+
const timestamp = parseTimeOrRelative(value, nowMs());
|
|
114
|
+
if (!Number.isFinite(timestamp))
|
|
115
|
+
fail(`Invalid ${flag} timestamp: ${value}`);
|
|
116
|
+
return timestamp;
|
|
117
|
+
}
|
|
118
|
+
function parseEcho(argv, viewer) {
|
|
119
|
+
let lastN = 10;
|
|
120
|
+
let lastNExplicit = false;
|
|
121
|
+
let before;
|
|
122
|
+
let after;
|
|
123
|
+
let afterIndex;
|
|
124
|
+
let atIndex;
|
|
125
|
+
let beforeContext;
|
|
126
|
+
let afterContext;
|
|
127
|
+
let mention;
|
|
128
|
+
let mentionsViewer = false;
|
|
129
|
+
let pending = false;
|
|
130
|
+
let full = false;
|
|
131
|
+
let grep;
|
|
132
|
+
let fixed;
|
|
133
|
+
let ids;
|
|
134
|
+
let order;
|
|
135
|
+
let format;
|
|
136
|
+
let countOnly = false;
|
|
137
|
+
let json = false;
|
|
138
|
+
const participants = [];
|
|
139
|
+
for (let index = 0; index < argv.length; index++) {
|
|
140
|
+
const flag = argv[index];
|
|
141
|
+
if (flag === '--last' || flag === '--limit') {
|
|
142
|
+
lastN = parsePositiveInteger(requireValue(argv, index, flag), flag);
|
|
143
|
+
lastNExplicit = true;
|
|
144
|
+
index += 1;
|
|
145
|
+
}
|
|
146
|
+
else if (flag === '--all') {
|
|
147
|
+
lastN = null;
|
|
148
|
+
lastNExplicit = true;
|
|
149
|
+
}
|
|
150
|
+
else if (flag === '--by' || flag === '--from') {
|
|
151
|
+
participants.push(...parseNameList(requireValue(argv, index, flag), flag));
|
|
152
|
+
index += 1;
|
|
153
|
+
}
|
|
154
|
+
else if (flag === '--before' || flag === '--until') {
|
|
155
|
+
before = parseTimestamp(requireValue(argv, index, flag), flag);
|
|
156
|
+
index += 1;
|
|
157
|
+
}
|
|
158
|
+
else if (flag === '--since') {
|
|
159
|
+
after = parseTimestamp(requireValue(argv, index, flag), flag);
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
else if (flag === '--after') {
|
|
163
|
+
afterIndex = parseActRef(requireValue(argv, index, flag), flag);
|
|
164
|
+
index += 1;
|
|
165
|
+
}
|
|
166
|
+
else if (flag === '--at') {
|
|
167
|
+
atIndex = parseActRef(requireValue(argv, index, flag), flag);
|
|
168
|
+
index += 1;
|
|
169
|
+
}
|
|
170
|
+
else if (flag === '-B') {
|
|
171
|
+
beforeContext = parseNonNegativeInteger(requireValue(argv, index, flag), flag);
|
|
172
|
+
index += 1;
|
|
173
|
+
}
|
|
174
|
+
else if (flag === '-A') {
|
|
175
|
+
afterContext = parseNonNegativeInteger(requireValue(argv, index, flag), flag);
|
|
176
|
+
index += 1;
|
|
177
|
+
}
|
|
178
|
+
else if (flag === '-C') {
|
|
179
|
+
const context = parseNonNegativeInteger(requireValue(argv, index, flag), flag);
|
|
180
|
+
beforeContext = context;
|
|
181
|
+
afterContext = context;
|
|
182
|
+
index += 1;
|
|
183
|
+
}
|
|
184
|
+
else if (flag === '--full')
|
|
185
|
+
full = true;
|
|
186
|
+
else if (flag === '--mention') {
|
|
187
|
+
mention = requireValue(argv, index, flag);
|
|
188
|
+
index += 1;
|
|
189
|
+
}
|
|
190
|
+
else if (flag === '--mentions') {
|
|
191
|
+
const value = requireValue(argv, index, flag);
|
|
192
|
+
if (value !== 'me')
|
|
193
|
+
fail(`Invalid --mentions: only 'me' is supported (got ${value}).`);
|
|
194
|
+
mentionsViewer = true;
|
|
195
|
+
index += 1;
|
|
196
|
+
}
|
|
197
|
+
else if (flag === '--pending')
|
|
198
|
+
pending = true;
|
|
199
|
+
else if (flag === '--grep') {
|
|
200
|
+
grep = requireValue(argv, index, flag);
|
|
201
|
+
index += 1;
|
|
202
|
+
}
|
|
203
|
+
else if (flag === '--fixed') {
|
|
204
|
+
fixed = requireValue(argv, index, flag);
|
|
205
|
+
index += 1;
|
|
206
|
+
}
|
|
207
|
+
else if (flag === '--ids') {
|
|
208
|
+
ids = requireValue(argv, index, flag)
|
|
209
|
+
.split(',')
|
|
210
|
+
.map((item) => item.trim())
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.map((item) => parseActRef(item, flag));
|
|
213
|
+
index += 1;
|
|
214
|
+
}
|
|
215
|
+
else if (flag === '--order') {
|
|
216
|
+
const value = requireValue(argv, index, flag);
|
|
217
|
+
if (value !== 'asc' && value !== 'desc')
|
|
218
|
+
fail('Invalid --order: expected asc or desc.');
|
|
219
|
+
order = value;
|
|
220
|
+
index += 1;
|
|
221
|
+
}
|
|
222
|
+
else if (flag === '--format') {
|
|
223
|
+
format = requireValue(argv, index, flag).split(',').map((item) => item.trim()).filter(Boolean);
|
|
224
|
+
index += 1;
|
|
225
|
+
}
|
|
226
|
+
else if (flag === '--count')
|
|
227
|
+
countOnly = true;
|
|
228
|
+
else if (flag === '--json')
|
|
229
|
+
json = true;
|
|
230
|
+
else
|
|
231
|
+
fail('✕ invalid arguments for echo');
|
|
232
|
+
}
|
|
233
|
+
if ((mentionsViewer || pending) && !viewer)
|
|
234
|
+
fail('--mentions me / --pending require --as <name>.');
|
|
235
|
+
if (grep !== undefined && fixed !== undefined)
|
|
236
|
+
fail('--grep and --fixed cannot be combined.');
|
|
237
|
+
if (grep === '' || fixed === '')
|
|
238
|
+
fail('--grep and --fixed require non-empty text.');
|
|
239
|
+
if (!lastNExplicit && (atIndex !== undefined || ids !== undefined || pending))
|
|
240
|
+
lastN = null;
|
|
241
|
+
return {
|
|
242
|
+
lastN,
|
|
243
|
+
participants,
|
|
244
|
+
before,
|
|
245
|
+
after,
|
|
246
|
+
afterIndex,
|
|
247
|
+
atIndex,
|
|
248
|
+
beforeContext,
|
|
249
|
+
afterContext,
|
|
250
|
+
mention,
|
|
251
|
+
mentionsViewer,
|
|
252
|
+
pending,
|
|
253
|
+
viewer,
|
|
254
|
+
full,
|
|
255
|
+
grep,
|
|
256
|
+
fixed,
|
|
257
|
+
ids,
|
|
258
|
+
order,
|
|
259
|
+
format,
|
|
260
|
+
countOnly,
|
|
261
|
+
json,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function renderFields(doc, item, fields) {
|
|
265
|
+
return fields.map((field) => {
|
|
266
|
+
switch (field) {
|
|
267
|
+
case 'id': return actId(item.index);
|
|
268
|
+
case 'author':
|
|
269
|
+
case 'actor': return item.act.actor ?? '';
|
|
270
|
+
case 'ts':
|
|
271
|
+
case 'at': return formatTimestamp(item.act.at);
|
|
272
|
+
case 'kind': return item.act.kind;
|
|
273
|
+
case 'body': return 'body' in item.act && typeof item.act.body === 'string' ? item.act.body.replace(/\s+/g, ' ').trim() : '';
|
|
274
|
+
case 'number': return item.act.kind === 'say' ? String(sayNumberFor(doc.acts, item.act)) : '';
|
|
275
|
+
default: return '';
|
|
276
|
+
}
|
|
277
|
+
}).join('\t');
|
|
278
|
+
}
|
|
279
|
+
function jsonLine(doc, item) {
|
|
280
|
+
const act = item.act;
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
id: actId(item.index),
|
|
283
|
+
index: item.index,
|
|
284
|
+
kind: act.kind,
|
|
285
|
+
author: act.actor ?? null,
|
|
286
|
+
at: act.at,
|
|
287
|
+
ts: formatTimestamp(act.at),
|
|
288
|
+
body: 'body' in act && typeof act.body === 'string' ? act.body : '',
|
|
289
|
+
number: act.kind === 'say' ? sayNumberFor(doc.acts, act) : null,
|
|
290
|
+
reach: act.kind === 'say' ? act.reach ?? null : null,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
export const echoCommand = {
|
|
294
|
+
parse(argv, context) { return parseEcho(argv, context.name); },
|
|
295
|
+
execute(options, context) {
|
|
296
|
+
const doc = loadSquare(context.squarePath);
|
|
297
|
+
let events = coreActivities(doc, options);
|
|
298
|
+
const searching = options.grep !== undefined || options.fixed !== undefined;
|
|
299
|
+
const totalMatches = searching ? events.length : 0;
|
|
300
|
+
if (options.lastN != null) {
|
|
301
|
+
events = options.order === 'desc'
|
|
302
|
+
? events.slice(0, options.lastN)
|
|
303
|
+
: events.slice(-options.lastN);
|
|
304
|
+
}
|
|
305
|
+
if (options.countOnly)
|
|
306
|
+
return `${searching ? totalMatches : events.length}\n`;
|
|
307
|
+
if (options.json)
|
|
308
|
+
return events.map((item) => jsonLine(doc, item)).join('\n') + (events.length > 0 ? '\n' : '');
|
|
309
|
+
if (options.format !== undefined && options.format.length > 0) {
|
|
310
|
+
return events.map((item) => renderFields(doc, item, options.format)).join('\n') + (events.length > 0 ? '\n' : '');
|
|
311
|
+
}
|
|
312
|
+
const pattern = options.grep ?? options.fixed;
|
|
313
|
+
const output = pattern === undefined || pattern === ''
|
|
314
|
+
? renderActivitiesView(doc, events, null, options.full, context.squarePath, options.viewer ?? '')
|
|
315
|
+
: renderGrepActivitiesView(events, totalMatches, options.full, context.squarePath, pattern, options.fixed !== undefined);
|
|
316
|
+
return withPathOutput(context.squarePath, output, { participantCount: inSquareCount(doc) });
|
|
317
|
+
},
|
|
318
|
+
present: (result) => process.stdout.write(result),
|
|
319
|
+
};
|
|
320
|
+
export const warmupCommand = {
|
|
321
|
+
parse(argv, context) { if (argv.length > 0)
|
|
322
|
+
usage(context.command); return undefined; },
|
|
323
|
+
execute(_intent, context) {
|
|
324
|
+
const doc = loadSquare(context.squarePath);
|
|
325
|
+
return withPathOutput(context.squarePath, doc.warmup.join('\n'), {
|
|
326
|
+
participantCount: inSquareCount(doc),
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
present: (result) => process.stdout.write(result),
|
|
330
|
+
};
|
|
331
|
+
export const participantsCommand = {
|
|
332
|
+
parse(argv, context) { if (argv.length > 0)
|
|
333
|
+
usage(context.command); return undefined; },
|
|
334
|
+
execute(_intent, context) {
|
|
335
|
+
const doc = loadSquare(context.squarePath);
|
|
336
|
+
const result = coreParticipants(doc, nowMs());
|
|
337
|
+
const lines = result.participants.map((participant) => {
|
|
338
|
+
const glyph = participant.state === 'done' ? '×' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 ? '●' : '○';
|
|
339
|
+
const state = participant.state === 'done' ? 'done' : participant.presence === 'watching' ? 'catching' : participant.state;
|
|
340
|
+
const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now);
|
|
341
|
+
return ` ${glyph} ${participant.name} · ${state} · ${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${last}`;
|
|
342
|
+
});
|
|
343
|
+
const participantCount = result.participants.filter((participant) => participant.state === 'active').length;
|
|
344
|
+
return withPathOutput(context.squarePath, ['participants', ...lines].join('\n'), {
|
|
345
|
+
participantCount,
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
present: (result) => process.stdout.write(result),
|
|
349
|
+
};
|
|
350
|
+
export const statusCommand = {
|
|
351
|
+
parse(argv, context) { if (argv.length > 0)
|
|
352
|
+
usage(context.command); return undefined; },
|
|
353
|
+
execute(_intent, context) {
|
|
354
|
+
const doc = loadSquare(context.squarePath);
|
|
355
|
+
const result = coreStatus(doc, nowMs());
|
|
356
|
+
const active = result.participants.filter((participant) => participant.state === 'active').sort((a, b) => {
|
|
357
|
+
const aViewer = context.name !== undefined && sameName(a.name, context.name);
|
|
358
|
+
const bViewer = context.name !== undefined && sameName(b.name, context.name);
|
|
359
|
+
if (aViewer !== bViewer)
|
|
360
|
+
return aViewer ? -1 : 1;
|
|
361
|
+
return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
|
|
362
|
+
});
|
|
363
|
+
const people = active.length === 0 ? [' ○ nobody in the square'] : active.map((participant) => {
|
|
364
|
+
const glyph = participant.presence === 'watching'
|
|
365
|
+
? '◎'
|
|
366
|
+
: participant.activityCount > 0 ? '●' : '○';
|
|
367
|
+
const summary = participant.activityCount > 0
|
|
368
|
+
? `${participant.activityCount} act${participant.activityCount === 1 ? '' : 's'} · ${participant.lastActiveAt === undefined
|
|
369
|
+
? 'just now'
|
|
370
|
+
: formatRelativeTime(participant.lastActiveAt, result.now)}`
|
|
371
|
+
: `quiet · ${participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, result.now)}`;
|
|
372
|
+
const showAttention = context.name === undefined || sameName(participant.name, context.name);
|
|
373
|
+
const attention = !showAttention
|
|
374
|
+
? ''
|
|
375
|
+
: participant.pendingMentionCount > 0
|
|
376
|
+
? `${participant.pendingMentionCount} mention${participant.pendingMentionCount === 1 ? '' : 's'} waiting`
|
|
377
|
+
: participant.unreadActivityCount > 0
|
|
378
|
+
? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
|
|
379
|
+
: 'caught up';
|
|
380
|
+
return ` ${glyph} ${participant.name} · ${summary}${attention === '' ? '' : ` · ${attention}`}`;
|
|
381
|
+
});
|
|
382
|
+
const cap = result.hardCap === null ? 'unlimited' : String(result.hardCap);
|
|
383
|
+
const hold = result.holdActive
|
|
384
|
+
? `· ${result.holdActor ?? 'someone'} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''} · ${result.holdAt === undefined
|
|
385
|
+
? 'just now'
|
|
386
|
+
: formatRelativeTime(result.holdAt, result.now)}`
|
|
387
|
+
: undefined;
|
|
388
|
+
const visible = result.latestAct === undefined
|
|
389
|
+
? ''
|
|
390
|
+
: renderVisibleEvent(doc.acts, result.latestAct, context.name ?? '', {
|
|
391
|
+
now: result.now,
|
|
392
|
+
preview: 200,
|
|
393
|
+
actNumber: result.latestAct.kind === 'say'
|
|
394
|
+
? sayNumberFor(doc.acts, result.latestAct)
|
|
395
|
+
: undefined,
|
|
396
|
+
});
|
|
397
|
+
const latest = visible === ''
|
|
398
|
+
? [result.latestAct === undefined
|
|
399
|
+
? ' ○ no public activity yet'
|
|
400
|
+
: ' · latest activity is private to another participant']
|
|
401
|
+
: [` ${visible.replace(/\n/g, '\n ')}`];
|
|
402
|
+
if (visible.includes('more chars') && result.latestAct !== undefined) {
|
|
403
|
+
const prefix = context.name === undefined ? commandPrefix(context.squarePath) : participantCommandPrefix(context.squarePath, context.name);
|
|
404
|
+
latest.push(`» ${prefix} echo --at ${actId(result.latestAct)} -C 2 --full`);
|
|
405
|
+
}
|
|
406
|
+
const output = [
|
|
407
|
+
`${result.activeCount} active · ${result.doneCount} done · cap ${cap} · throttle ${result.throttlePerMinute === undefined ? 'none' : `${result.throttlePerMinute}/min`}`,
|
|
408
|
+
...(hold === undefined ? [] : ['', hold]), '', 'around the square', ...people, '', 'latest', ...latest,
|
|
409
|
+
].join('\n');
|
|
410
|
+
return withPathOutput(context.squarePath, output, { participantCount: result.activeCount, held: result.holdActive });
|
|
411
|
+
},
|
|
412
|
+
present: (result) => process.stdout.write(result),
|
|
413
|
+
};
|
|
414
|
+
export const inboxCommand = {
|
|
415
|
+
parse(argv, context) {
|
|
416
|
+
let sessionId;
|
|
417
|
+
let json = false;
|
|
418
|
+
for (let index = 0; index < argv.length; index++) {
|
|
419
|
+
if (argv[index] === '--for-session') {
|
|
420
|
+
sessionId = requireValue(argv, index, argv[index]);
|
|
421
|
+
index += 1;
|
|
422
|
+
}
|
|
423
|
+
else if (argv[index] === '--json')
|
|
424
|
+
json = true;
|
|
425
|
+
else
|
|
426
|
+
usage(context.command);
|
|
427
|
+
}
|
|
428
|
+
if (!sessionId)
|
|
429
|
+
fail('inbox requires --for-session <session-id>.');
|
|
430
|
+
return { sessionId, json };
|
|
431
|
+
},
|
|
432
|
+
execute(intent) {
|
|
433
|
+
const inbox = sessionInbox(intent.sessionId);
|
|
434
|
+
return intent.json ? `${JSON.stringify(inbox)}\n` : inbox.map((membership) => `${membership.name}\t${membership.squarePath}\t${membership.notifications.length}\n`).join('');
|
|
435
|
+
},
|
|
436
|
+
present: (result) => process.stdout.write(result),
|
|
437
|
+
};
|
|
438
|
+
function hookCommand(runHook) {
|
|
439
|
+
return {
|
|
440
|
+
parse(argv, context) { if (argv.length > 0)
|
|
441
|
+
usage(context.command); return undefined; },
|
|
442
|
+
execute: () => runHook(readStdinSync()),
|
|
443
|
+
present: (result) => process.stdout.write(result),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
export const claudeHookCommand = hookCommand(runClaudeHook);
|
|
447
|
+
export const codexHookCommand = hookCommand(runCodexHook);
|
|
448
|
+
/** Maintain the local discovery cache before participant-facing adapters run. */
|
|
449
|
+
export function refreshLocalRegistration(squarePath, name) {
|
|
450
|
+
if (name === undefined)
|
|
451
|
+
return;
|
|
452
|
+
try {
|
|
453
|
+
const doc = loadSquare(squarePath);
|
|
454
|
+
const known = resolveRosterName(doc, name);
|
|
455
|
+
if (known !== undefined && isCurrentlyJoined(doc.acts, known))
|
|
456
|
+
recordLocalJoin(known, squarePath);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// The machine-local discovery cache never makes a Square command fail.
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { helpRequest } from '../help.js';
|
|
2
|
+
import { SquareError } from '../model.js';
|
|
3
|
+
import { defaultContext, parseGlobalArgs } from './context.js';
|
|
4
|
+
import { refreshLocalRegistration } from './observation-commands.js';
|
|
5
|
+
import { executeRegisteredCommand, findCommand } from './registry.js';
|
|
6
|
+
function isMutatingCommand(command, argv) {
|
|
7
|
+
if (['build', 'join', 'catch', 'act', 'done', 'hold', 'resume', 'compact'].includes(command))
|
|
8
|
+
return true;
|
|
9
|
+
return command === 'doctor' && argv.some((argument) => argument === '--fix' || argument === 'reconcile-backlog');
|
|
10
|
+
}
|
|
11
|
+
function handleSquareError(error) {
|
|
12
|
+
if (error instanceof SquareError) {
|
|
13
|
+
process.stderr.write(`${error.message}\n`);
|
|
14
|
+
process.exit(error.code === 'not_found' ? 1 : 2);
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
/** Parse global flags, select an executable adapter, and leave all command work to the registry. */
|
|
19
|
+
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
20
|
+
try {
|
|
21
|
+
const requestedHelp = helpRequest(rawArgs);
|
|
22
|
+
if (requestedHelp !== undefined) {
|
|
23
|
+
await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help', '.square/SQUARE.md'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const parsed = parseGlobalArgs(rawArgs);
|
|
27
|
+
if (parsed.args.length === 0 || parsed.args[0] === '--help' || parsed.args[0] === '-h') {
|
|
28
|
+
await executeRegisteredCommand('help', [], defaultContext('help', parsed.squarePath, parsed.name));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const command = parsed.args[0];
|
|
32
|
+
if (findCommand(command) === undefined) {
|
|
33
|
+
process.stderr.write(`unknown command: ${command}\nrun 'square' for usage\n`);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
if (!parsed.explicitSquarePath && parsed.multipleSquares && isMutatingCommand(command, parsed.args.slice(1))) {
|
|
37
|
+
process.stderr.write('✕ more than one square is active here; choose the path before changing or consuming activity.\n» square ls\n');
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
if (['act', 'catch', 'done', 'hold', 'resume'].includes(command)) {
|
|
41
|
+
refreshLocalRegistration(parsed.squarePath, parsed.name);
|
|
42
|
+
}
|
|
43
|
+
await executeRegisteredCommand(command, parsed.args.slice(1), defaultContext(command, parsed.squarePath, parsed.name));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
handleSquareError(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { actCommand, buildCommand, compactCommand, doneCommand, holdCommand, joinCommand, resumeCommand } from './square-commands.js';
|
|
2
|
+
import { doctorCommand } from './maintenance-commands.js';
|
|
3
|
+
import { harnessCommand } from './harness-command.js';
|
|
4
|
+
import { helpCommand, versionCommand } from './meta-commands.js';
|
|
5
|
+
import { catchCommand, claudeHookCommand, codexHookCommand, echoCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, warmupCommand, } from './observation-commands.js';
|
|
6
|
+
/** Every public command is an executable adapter, including aliases and utility commands. */
|
|
7
|
+
export const commandRegistry = [
|
|
8
|
+
{ names: ['build'], spec: buildCommand },
|
|
9
|
+
{ names: ['ls', 'list'], spec: listCommand },
|
|
10
|
+
{ names: ['join'], spec: joinCommand },
|
|
11
|
+
{ names: ['stream'], spec: streamCommand },
|
|
12
|
+
{ names: ['inbox'], spec: inboxCommand },
|
|
13
|
+
{ names: ['claude-hook'], spec: claudeHookCommand },
|
|
14
|
+
{ names: ['codex-hook'], spec: codexHookCommand },
|
|
15
|
+
{ names: ['catch'], spec: catchCommand },
|
|
16
|
+
{ names: ['act'], spec: actCommand },
|
|
17
|
+
{ names: ['done'], spec: doneCommand },
|
|
18
|
+
{ names: ['hold'], spec: holdCommand },
|
|
19
|
+
{ names: ['resume'], spec: resumeCommand },
|
|
20
|
+
{ names: ['harness'], spec: harnessCommand },
|
|
21
|
+
{ names: ['compact'], spec: compactCommand },
|
|
22
|
+
{ names: ['doctor'], spec: doctorCommand },
|
|
23
|
+
{ names: ['echo'], spec: echoCommand },
|
|
24
|
+
{ names: ['warmup'], spec: warmupCommand },
|
|
25
|
+
{ names: ['status'], spec: statusCommand },
|
|
26
|
+
{ names: ['participants'], spec: participantsCommand },
|
|
27
|
+
{ names: ['help'], spec: helpCommand },
|
|
28
|
+
{ names: ['version', '--version', '-v'], spec: versionCommand },
|
|
29
|
+
];
|
|
30
|
+
export function findCommand(name) {
|
|
31
|
+
return commandRegistry.find((command) => command.names.includes(name));
|
|
32
|
+
}
|
|
33
|
+
export async function executeRegisteredCommand(name, argv, context) {
|
|
34
|
+
const command = findCommand(name);
|
|
35
|
+
if (command === undefined)
|
|
36
|
+
throw new Error(`No registered command named ${name}`);
|
|
37
|
+
const intent = command.spec.parse(argv, context);
|
|
38
|
+
const result = await command.spec.execute(intent, context);
|
|
39
|
+
command.spec.present(result, context);
|
|
40
|
+
}
|