@astrosheep/square 0.4.4 → 0.4.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.
Files changed (98) hide show
  1. package/dist/artifact-store.d.ts +1 -1
  2. package/dist/artifact-store.js +32 -15
  3. package/dist/artifact.d.ts +1 -4
  4. package/dist/artifact.js +0 -28
  5. package/dist/attention-presentation.js +1 -1
  6. package/dist/boundary-presentation.js +2 -2
  7. package/dist/claude-boundary-state.d.ts +6 -6
  8. package/dist/claude-boundary-state.js +13 -13
  9. package/dist/claude-hook.js +25 -24
  10. package/dist/claude-mcp-context.d.ts +2 -2
  11. package/dist/claude-mcp-context.js +6 -6
  12. package/dist/cli/context.js +3 -6
  13. package/dist/cli/json.js +1 -1
  14. package/dist/cli/mcp-server-command.js +3 -3
  15. package/dist/cli/observation-commands.d.ts +2 -2
  16. package/dist/cli/observation-commands.js +23 -51
  17. package/dist/cli/perceive-presentation.d.ts +3 -0
  18. package/dist/cli/{catch-presentation.js → perceive-presentation.js} +3 -3
  19. package/dist/cli/registry.js +2 -2
  20. package/dist/codex-address.d.ts +3 -0
  21. package/dist/codex-address.js +7 -0
  22. package/dist/codex-hook.d.ts +1 -1
  23. package/dist/codex-hook.js +11 -8
  24. package/dist/codex-queue.d.ts +3 -2
  25. package/dist/codex-queue.js +8 -11
  26. package/dist/codex-watch.d.ts +6 -1
  27. package/dist/codex-watch.js +27 -29
  28. package/dist/delivery-ledger-sqlite.d.ts +25 -20
  29. package/dist/delivery-ledger-sqlite.js +76 -61
  30. package/dist/delivery-ledger.d.ts +24 -19
  31. package/dist/delivery-ledger.js +150 -81
  32. package/dist/delivery-lifetime.d.ts +2 -2
  33. package/dist/delivery-lifetime.js +6 -18
  34. package/dist/help.js +12 -12
  35. package/dist/hints.d.ts +2 -2
  36. package/dist/hints.js +7 -7
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/list.d.ts +2 -2
  40. package/dist/list.js +4 -3
  41. package/dist/mcp.d.ts +2 -2
  42. package/dist/mcp.js +8 -32
  43. package/dist/neutral.d.ts +5 -5
  44. package/dist/notifications.js +12 -6
  45. package/dist/operation-presentation.d.ts +8 -3
  46. package/dist/operation-presentation.js +50 -12
  47. package/dist/packages/agent-delivery/src/file-lock.js +4 -1
  48. package/dist/perceive-decisions.d.ts +28 -0
  49. package/dist/{catch-decisions.js → perceive-decisions.js} +24 -24
  50. package/dist/{catch-selection.d.ts → perceive-selection.d.ts} +2 -2
  51. package/dist/{catch-selection.js → perceive-selection.js} +3 -3
  52. package/dist/plugins/opencode/index.js +4 -4
  53. package/dist/plugins/opencode-shared/delivery.d.ts +6 -6
  54. package/dist/plugins/opencode-shared/delivery.js +27 -21
  55. package/dist/plugins/opencode-v1/runtime.js +3 -3
  56. package/dist/ports.d.ts +0 -4
  57. package/dist/presentation.d.ts +1 -1
  58. package/dist/presentation.js +13 -13
  59. package/dist/session-lifecycle.d.ts +2 -2
  60. package/dist/session-lifecycle.js +11 -11
  61. package/dist/sqlite-square.d.ts +2 -4
  62. package/dist/sqlite-square.js +47 -54
  63. package/dist/sqlite-transaction.d.ts +2 -0
  64. package/dist/sqlite-transaction.js +25 -3
  65. package/dist/square-api.d.ts +1 -1
  66. package/dist/square-api.js +1 -1
  67. package/dist/square-domain.js +1 -1
  68. package/dist/square-facade.d.ts +15 -13
  69. package/dist/square-location.js +1 -1
  70. package/dist/square-posting.d.ts +5 -5
  71. package/dist/square-posting.js +39 -38
  72. package/dist/square-tools.d.ts +15 -4
  73. package/dist/square-tools.js +35 -17
  74. package/dist/views.d.ts +7 -3
  75. package/dist/views.js +6 -3
  76. package/guides/architect.md +2 -2
  77. package/guides/change-waits.md +1 -1
  78. package/guides/codex-delivery.md +40 -35
  79. package/guides/delivery-primitives.md +22 -7
  80. package/guides/mcp.md +4 -2
  81. package/guides/operation-cancellation.md +11 -3
  82. package/guides/pi-native-delivery.md +1 -1
  83. package/guides/pi-tools.md +11 -6
  84. package/guides/receive-primitive.md +3 -3
  85. package/guides/square-api.md +15 -8
  86. package/package.json +2 -2
  87. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  88. package/plugins/claude/skills/square/SKILL.md +10 -10
  89. package/plugins/codex/.codex-plugin/plugin.json +2 -2
  90. package/plugins/codex/hooks/hooks.json +3 -0
  91. package/plugins/opencode/index.ts +4 -4
  92. package/plugins/opencode-shared/delivery.ts +27 -21
  93. package/plugins/opencode-v1/runtime.ts +3 -3
  94. package/plugins/pi/square-pi.js +18 -12
  95. package/skills/square/SKILL.md +10 -10
  96. package/dist/catch-decisions.d.ts +0 -28
  97. package/dist/cli/catch-presentation.d.ts +0 -3
  98. package/guides/mcp-connection-lifetime.md +0 -22
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { validateOwner } from './neutral.js';
4
4
  import { observeFileMetadata } from './file-changes.js';
5
5
  import { LedgerStore } from './delivery-ledger-sqlite.js';
6
+ import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
6
7
  function nonempty(value) {
7
8
  if (typeof value !== 'string' || value.length === 0)
8
9
  throw new TypeError('Expected a nonempty string');
@@ -59,6 +60,8 @@ function encodeDetails(details) {
59
60
  export class DeliveryLedger {
60
61
  store;
61
62
  leaseMs;
63
+ settlements = new Set();
64
+ retired = false;
62
65
  constructor(store, leaseMs) {
63
66
  this.store = store;
64
67
  this.leaseMs = leaseMs;
@@ -69,79 +72,103 @@ export class DeliveryLedger {
69
72
  throw new TypeError('Delivery location must be absolute');
70
73
  const leaseMs = options.leaseMs ?? 30_000;
71
74
  positive(leaseMs);
72
- return new DeliveryLedger(await LedgerStore.open(location, options.clock ?? Date.now), leaseMs);
75
+ return new DeliveryLedger(await LedgerStore.open(location, options.clock ?? Date.now, options.signal), leaseMs);
73
76
  }
74
- close() { return this.store.close(); }
75
- attach(input) {
77
+ close() {
78
+ this.retired = true;
79
+ this.store.retire();
80
+ return this.settlements.size ? Promise.resolve() : this.store.close();
81
+ }
82
+ retainSettlement(handle) {
83
+ this.settlements.add(handle);
84
+ void handle.then(() => {
85
+ this.settlements.delete(handle);
86
+ if (this.retired && !this.settlements.size)
87
+ void this.store.close().catch(() => this.diagnose('delivery-ledger-deferred-close-error'));
88
+ });
89
+ }
90
+ diagnose(event, token, persistence) {
91
+ try {
92
+ const diagnostic = redactCurrentDiagnostic({ event, token, persistence });
93
+ try {
94
+ console.warn(diagnostic);
95
+ }
96
+ catch {
97
+ process.stderr.write(`${JSON.stringify(diagnostic)}\n`);
98
+ }
99
+ }
100
+ catch { /* Diagnostics cannot prevent resource retirement. */ }
101
+ }
102
+ attach(input, signal) {
76
103
  validateOwner(input.owner);
77
104
  routes(input.routes);
78
105
  if (input.expectedGeneration !== null)
79
106
  nonempty(input.expectedGeneration);
80
- return this.store.attach(input);
107
+ return this.store.attach(input, signal);
81
108
  }
82
- updateRoutes(input) {
109
+ updateRoutes(input, signal) {
83
110
  validateOwner(input.owner);
84
111
  nonempty(input.generation);
85
112
  routes(input.routes);
86
- return this.store.updateRoutes(input);
113
+ return this.store.updateRoutes(input, signal);
87
114
  }
88
- detach(input) { return this.lifecycle(input, 'detached'); }
89
- markDeleted(input) { return this.lifecycle(input, 'deleted'); }
90
- lifecycle(input, state) {
115
+ detach(input, signal) { return this.lifecycle(input, 'detached', signal); }
116
+ markDeleted(input, signal) { return this.lifecycle(input, 'deleted', signal); }
117
+ lifecycle(input, state, signal) {
91
118
  validateOwner(input.owner);
92
119
  nonempty(input.generation);
93
- return this.store.lifecycle(input, state);
120
+ return this.store.lifecycle(input, state, signal);
94
121
  }
95
- session(owner) { validateOwner(owner); return this.store.session(owner); }
96
- sessions() { return this.store.sessions(); }
97
- pending(owner, options) {
122
+ session(owner, signal) { validateOwner(owner); return this.store.session(owner, signal); }
123
+ sessions(signal) { return this.store.sessions(signal); }
124
+ pending(owner, options, signal) {
98
125
  validateOwner(owner);
99
126
  nonempty(options.generation);
100
- return this.store.pendingCards(owner, options.generation);
127
+ return this.store.pendingCards(owner, options.generation, signal);
101
128
  }
102
- handoffs(owner, options) {
129
+ handoffs(owner, options, signal) {
103
130
  validateOwner(owner);
104
131
  nonempty(options.generation);
105
- return this.store.handoffs(owner, options.generation);
132
+ return this.store.handoffs(owner, options.generation, signal);
106
133
  }
107
- post(input) {
134
+ post(input, signal) {
108
135
  validateOwner(input.owner);
109
136
  nonempty(input.key);
110
137
  if (typeof input.text !== 'string')
111
138
  throw new TypeError('Expected card text');
112
- return this.store.post({ ...input, details: input.details === undefined ? null : encodeDetails(input.details) });
139
+ return this.store.post({ ...input, details: input.details === undefined ? null : encodeDetails(input.details) }, signal);
113
140
  }
114
- suppress(input) {
141
+ suppress(input, signal) {
115
142
  validateOwner(input.owner);
116
143
  nonempty(input.key);
117
144
  if (!['exact', 'prefix'].includes(input.scope) || input.expiresAt !== undefined && !Number.isFinite(input.expiresAt))
118
145
  throw new TypeError('Invalid suppression');
119
- return this.store.suppress(input);
146
+ return this.store.suppress(input, signal);
120
147
  }
121
- renewSuppression(input) {
148
+ renewSuppression(input, signal) {
122
149
  validateOwner(input.owner);
123
150
  nonempty(input.id);
124
151
  if (!Number.isFinite(input.expiresAt))
125
152
  throw new TypeError('Expected finite suppression expiry');
126
- return this.store.renewSuppression(input);
153
+ return this.store.renewSuppression(input, signal);
127
154
  }
128
- releaseSuppression(input) {
155
+ releaseSuppression(input, signal) {
129
156
  validateOwner(input.owner);
130
157
  nonempty(input.id);
131
- return this.store.releaseSuppression(input);
158
+ return this.store.releaseSuppression(input, signal);
132
159
  }
133
- cancel(input) {
160
+ cancel(input, signal) {
134
161
  validateOwner(input.owner);
135
162
  nonempty(input.generation);
136
- return this.store.cancel(input);
163
+ return this.store.cancel(input, signal);
137
164
  }
138
- receipts(owner, options = {}) {
165
+ receipts(owner, options = {}, signal) {
139
166
  validateOwner(owner);
140
167
  if (options.keys)
141
168
  options.keys.forEach(nonempty);
142
169
  if (options.since !== undefined && !Number.isFinite(options.since))
143
170
  throw new TypeError('Expected finite receipt time');
144
- return this.store.receipts(owner, options);
171
+ return this.store.receipts(owner, options, signal);
145
172
  }
146
173
  async claimPresentation(owner, options) {
147
174
  validateOwner(owner);
@@ -158,27 +185,27 @@ export class DeliveryLedger {
158
185
  settle: outcome => {
159
186
  if (!['presented', 'failed', 'unknown', 'released'].includes(outcome))
160
187
  throw new TypeError('Invalid presentation outcome');
161
- return this.store.settle(tokens, outcome);
188
+ return this.store.settle(tokens, outcome, AbortSignal.timeout(5000));
162
189
  },
163
190
  };
164
191
  }
165
192
  /** Resolve the original prepared handoff, including its stored route kind. */
166
- handoff(token) {
193
+ handoff(token, signal) {
167
194
  nonempty(token);
168
- return this.store.handoff(token);
195
+ return this.store.handoff(token, signal);
169
196
  }
170
- guardHandoff(ref) { return this.store.guardHandoff(ref); }
171
- acceptHandoff(ref) { return this.store.acceptHandoff(ref); }
197
+ guardHandoff(ref, signal) { return this.store.guardHandoff(ref, signal); }
198
+ acceptHandoff(ref, signal) { return this.store.acceptHandoff(ref, signal); }
172
199
  /** Confirms the exact existing external effect; never sends or returns text. */
173
- observePresentation(handoff, options) {
200
+ observePresentation(handoff, options, signal) {
174
201
  if (typeof handoff === 'string')
175
202
  nonempty(handoff);
176
- return this.store.observePresentation(handoff, options);
203
+ return this.store.observePresentation(handoff, options, signal);
177
204
  }
178
- health(owner) {
205
+ health(owner, signal) {
179
206
  if (owner !== undefined)
180
207
  validateOwner(owner);
181
- return this.store.health(owner);
208
+ return this.store.health(owner, signal);
182
209
  }
183
210
  /** Subscribe first; read the returned version, inspect state, then await changed(version,...). */
184
211
  observe(owner) {
@@ -193,76 +220,118 @@ export class DeliveryLedger {
193
220
  if (options.signal?.aborted)
194
221
  return { outcome: 'not-invoked' };
195
222
  nonempty(transport.kind);
196
- const claim = await this.store.claimHandoff(owner, key, options.generation, transport.kind, Math.max(this.leaseMs, options.timeoutMs), options.signal);
197
- if (!claim)
198
- return { outcome: 'not-invoked' };
199
- const token = claim.attempt.token;
223
+ const lifetime = Promise.withResolvers();
224
+ this.retainSettlement(lifetime.promise);
225
+ // One deadline includes acquisition, validation, preparation and the external effect.
226
+ const deadline = Date.now() + options.timeoutMs;
200
227
  const controller = new AbortController();
201
228
  const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
202
- const deadline = Date.now() + options.timeoutMs;
203
- let persistenceFailure;
204
- const persist = async (operation) => {
229
+ const timer = setTimeout(() => controller.abort(new Error('Delivery deadline elapsed')), options.timeoutMs);
230
+ let onAbort;
231
+ try {
232
+ let claim;
205
233
  try {
206
- return await operation;
234
+ claim = await this.store.claimHandoff(owner, key, options.generation, transport.kind, Math.max(this.leaseMs, options.timeoutMs), signal);
207
235
  }
208
236
  catch (error) {
209
- persistenceFailure = { error };
237
+ if (signal.aborted && error === signal.reason)
238
+ return { outcome: 'not-invoked' };
210
239
  throw error;
211
240
  }
212
- };
213
- const current = () => persist(this.store.current([token], signal, claim.route));
214
- const finish = async (outcome) => {
215
- if (!['accepted', 'presented', 'failed', 'unknown', 'not-capable', 'released'].includes(outcome))
216
- throw new TypeError('Invalid transport outcome');
217
- const recorded = await this.store.settle([token], outcome === 'not-capable' ? 'released' : outcome);
218
- if (persistenceFailure)
219
- throw persistenceFailure.error;
220
- return { outcome, token, persistence: recorded ? 'recorded' : 'not-recorded' };
221
- };
222
- if (!await current() || signal.aborted || Date.now() >= deadline)
223
- return finish('released');
224
- let timer;
225
- let onAbort;
226
- try {
241
+ if (!claim)
242
+ return { outcome: 'not-invoked' };
243
+ const token = claim.attempt.token;
244
+ let persistenceFailure;
245
+ const persist = async (operation) => {
246
+ try {
247
+ return await operation;
248
+ }
249
+ catch (error) {
250
+ if (!signal.aborted || error !== signal.reason)
251
+ persistenceFailure = { error };
252
+ throw error;
253
+ }
254
+ };
255
+ const current = () => persist(this.store.current([token], signal, claim.route));
256
+ const finish = async (outcome) => {
257
+ if (!['accepted', 'presented', 'failed', 'unknown', 'not-capable', 'released'].includes(outcome))
258
+ throw new TypeError('Invalid transport outcome');
259
+ const recorded = await this.store.settle([token], outcome === 'not-capable' ? 'released' : outcome, AbortSignal.timeout(5000));
260
+ return { outcome, token, persistence: recorded ? 'recorded' : 'not-recorded' };
261
+ };
262
+ let eligible;
263
+ try {
264
+ eligible = await current();
265
+ }
266
+ catch (error) {
267
+ const released = await finish('released');
268
+ if (signal.aborted && error === signal.reason)
269
+ return released;
270
+ throw error;
271
+ }
272
+ if (this.retired || !eligible || signal.aborted || Date.now() >= deadline)
273
+ return await finish('released');
227
274
  const interrupted = new Promise(resolve => {
228
275
  onAbort = () => resolve('unknown');
229
276
  signal.addEventListener('abort', onAbort, { once: true });
230
- timer = setTimeout(() => { resolve('unknown'); controller.abort(new Error('Delivery deadline elapsed')); }, options.timeoutMs);
277
+ if (signal.aborted)
278
+ onAbort();
231
279
  });
232
- // Invoke synchronously after the final fence; adapters own checks after their own awaits.
233
- const effect = (async () => {
234
- if (signal.aborted || Date.now() >= deadline)
235
- return 'released';
236
- return transport.send(claim.card, claim.route.address, { token, deadline, signal,
237
- beforeSend: current,
238
- prepare: value => { correlation(value); return persist(this.store.prepare(token, value, signal)); },
239
- });
240
- })().catch(() => 'unknown');
280
+ // No await separates the final fence from invocation. Adapters fence their own awaits.
281
+ const effect = (async () => transport.send(claim.card, claim.route.address, { token, deadline, signal,
282
+ beforeSend: current,
283
+ prepare: value => { correlation(value); return persist(this.store.prepare(token, value, signal)); },
284
+ }))().catch(() => 'unknown');
241
285
  const first = await Promise.race([
242
286
  effect.then(outcome => ({ outcome, interrupted: false })),
243
287
  interrupted.then(outcome => ({ outcome, interrupted: true })),
244
288
  ]);
245
- if (!first.interrupted)
246
- return await finish(first.outcome);
289
+ if (!first.interrupted) {
290
+ const result = await finish(first.outcome);
291
+ if (persistenceFailure)
292
+ throw persistenceFailure.error;
293
+ return result;
294
+ }
295
+ // Register the effect followup even if the initial unknown write fails. Its
296
+ // captured connection survives caller close; no new send authority is granted.
247
297
  const unknown = finish('unknown');
248
- // Register before awaiting persistence; even an already completed effect waits for unknown.
249
- const lateSettlement = unknown.then(async () => {
298
+ let unknownRecorded = false;
299
+ const initial = unknown.then(result => { unknownRecorded = result.persistence === 'recorded'; return result; });
300
+ const lateSettlement = initial.catch(() => undefined).then(async () => {
250
301
  const outcome = await effect;
251
302
  try {
252
- const result = await finish(outcome);
253
- return { outcome, persistence: { status: result.persistence } };
303
+ // A late negative cannot release an interrupted effect whose unknown
304
+ // evidence failed to persist. Keep conservative custody in that case.
305
+ const negative = ['failed', 'not-capable', 'released'].includes(outcome);
306
+ const result = await finish(!unknownRecorded && negative ? 'unknown' : outcome);
307
+ return { outcome, persistence: { status: !unknownRecorded && negative ? 'not-recorded' : result.persistence } };
254
308
  }
255
309
  catch (error) {
256
310
  return { outcome, persistence: { status: 'error', error } };
257
311
  }
258
- }, error => ({ outcome: 'unknown', persistence: { status: 'error', error } }));
259
- return { ...await unknown, lateSettlement };
312
+ });
313
+ // The ledger owns retention on rejection too: callers cannot track a result
314
+ // they never received. Surface the original error while retaining late evidence.
315
+ this.retainSettlement(lateSettlement);
316
+ try {
317
+ const result = await initial;
318
+ if (persistenceFailure)
319
+ throw persistenceFailure.error;
320
+ return { ...result, lateSettlement };
321
+ }
322
+ catch (error) {
323
+ void lateSettlement.then(result => {
324
+ if (result.persistence.status !== 'recorded')
325
+ this.diagnose('delivery-late-settlement', token, result.persistence.status);
326
+ });
327
+ throw error;
328
+ }
260
329
  }
261
330
  finally {
262
- if (timer !== undefined)
263
- clearTimeout(timer);
331
+ clearTimeout(timer);
264
332
  if (onAbort)
265
333
  signal.removeEventListener('abort', onAbort);
334
+ lifetime.resolve();
266
335
  }
267
336
  }
268
337
  }
@@ -1,6 +1,6 @@
1
1
  import type { DeliveryLedger, HandoffResult } from './delivery-ledger.js';
2
- /** Retire the caller's connection without abandoning an already-invoked effect. */
2
+ /** Report late evidence; DeliveryLedger alone owns connection retirement. */
3
3
  export declare function retainDeliveryLedger(ledger: DeliveryLedger): {
4
4
  track(result: HandoffResult): void;
5
- close(): Promise<void>;
5
+ close: () => Promise<void>;
6
6
  };
@@ -1,8 +1,8 @@
1
1
  import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
2
- /** Retire the caller's connection without abandoning an already-invoked effect. */
2
+ /** Report late evidence; DeliveryLedger alone owns connection retirement. */
3
3
  export function retainDeliveryLedger(ledger) {
4
- const pending = new Set();
5
- let retired = false, closing;
4
+ const tracked = new WeakSet();
5
+ let closing;
6
6
  const diagnose = (event, result, detail) => {
7
7
  try {
8
8
  const diagnostic = redactCurrentDiagnostic({ event, token: result?.token, outcome: result?.outcome, ...detail });
@@ -15,31 +15,19 @@ export function retainDeliveryLedger(ledger) {
15
15
  }
16
16
  catch { /* Closed diagnostic sinks cannot prevent settlement or resource retirement. */ }
17
17
  };
18
- const closeOnce = () => closing ??= ledger.close();
19
- const settled = (handle) => {
20
- pending.delete(handle);
21
- if (retired && !pending.size)
22
- void closeOnce().catch(() => diagnose('delivery-ledger-deferred-close-error'));
23
- };
24
18
  return {
25
19
  track(result) {
26
20
  const handle = result.lateSettlement;
27
- if (!handle || pending.has(handle))
21
+ if (!handle || tracked.has(handle))
28
22
  return;
29
- pending.add(handle);
23
+ tracked.add(handle);
30
24
  void handle.then(late => {
31
25
  if (late.persistence.status !== 'recorded')
32
26
  diagnose('delivery-late-settlement', result, { lateOutcome: late.outcome, persistence: late.persistence.status });
33
- settled(handle);
34
27
  }, () => {
35
28
  diagnose('delivery-late-settlement-rejected', result);
36
- settled(handle);
37
29
  });
38
30
  },
39
- async close() {
40
- retired = true;
41
- if (!pending.size)
42
- await closeOnce();
43
- },
31
+ close: () => closing ??= ledger.close(),
44
32
  };
45
33
  }
package/dist/help.js CHANGED
@@ -2,14 +2,14 @@ import { JSON_COMMANDS } from './cli/json.js';
2
2
  const COMMANDS = [
3
3
  { names: ['help'], usage: 'help [command]', summary: 'Show the command index or help for one command.' },
4
4
  { names: ['version'], usage: 'version', summary: 'Print the installed version.', hiddenFromIndex: true },
5
- { names: ['mcp-server'], usage: 'mcp-server', summary: 'Start the MCP stdio server.', usesSquare: true, hiddenFromIndex: true, details: ['Set SQUARE_LOCATION or pass --location before the command.', 'Participant tools accept as on each call. Set the default with --as.', 'Session identity comes from request _meta.threadId (Codex) or _meta["ai.opencode/sessionID"] (OpenCode), falling back to the startup environment.'] },
5
+ { names: ['mcp-server'], usage: 'mcp-server', summary: 'Start the MCP stdio server.', hiddenFromIndex: true, details: ['Use list to find nearby squares; single-square tools require location on each call.', 'Participant tools accept as on each call; join requires an explicit name.', 'Session identity comes from request _meta.threadId (Codex) or _meta["ai.opencode/sessionID"] (OpenCode), falling back to the startup environment.'] },
6
6
  {
7
7
  names: ['build'], usage: 'build [--cap <N|unlimited>] [--template <name>] [--throttle N] [-f] < body.md', usesSquare: true, group: 'host',
8
8
  summary: 'Create a square from Markdown on stdin.',
9
9
  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.'],
10
10
  },
11
11
  {
12
- names: ['list', 'ls'], usage: '{command} [--depth N] [--limit N] [--after path]', summary: 'List nearby squares below the current directory.', group: 'host',
12
+ names: ['list', 'ls'], usage: '{command} [--depth N] [--limit N] [--after path]', summary: 'List nearby squares and your joined names with waiting activity counts.', group: 'host',
13
13
  details: ['Options:', ' --depth <N> Descend through at most N directory levels (default 4; maximum 16; 0 scans only the current directory).', ' --limit <N> Show one page (default 20, maximum 100).', ' --after <path> Resume after a relative path from a previous page.'],
14
14
  },
15
15
  {
@@ -31,7 +31,7 @@ const COMMANDS = [
31
31
  ' Bare activities are delivered only to participants listening to the sender.',
32
32
  '',
33
33
  'Options:',
34
- ' -f, --force Express without first catching unread activity or satisfying attention etiquette.',
34
+ ' -f, --force Express without first perceiving unread activity or satisfying attention etiquette.',
35
35
  ' --no-wait If throttled, save a draft and return instead of waiting.',
36
36
  ' --metadata <json> Attach a bounded JSON object (at most 16 KiB UTF-8 and 256 nesting levels); it may be the only content.',
37
37
  ' Metadata is returned in structured activity results but is not shown in human text or searchable.',
@@ -42,14 +42,14 @@ const COMMANDS = [
42
42
  { names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Stop hearing one participant\'s future activity, except bells.' },
43
43
  { names: ['listening'], usage: '--as <name> listening', usesSquare: true, group: 'participant', summary: 'Show who you are turned toward.', details: ['Listening applies to future activity. What you could hear when an activity landed does not change afterward.'] },
44
44
  {
45
- names: ['catch'], usage: '--as <name> catch [--id <activity-id>] [--now | --idle <duration>] [--from <names>] [--mention] [--limit <count>] [--replace]', usesSquare: true, group: 'participant',
46
- summary: 'Catch directed conversation since you last looked.',
47
- details: ['Modes:', ' --id <activity-id> Catch exactly one activity addressed to you, with its full body; implies --now.', ' Marks only this activity seen; repeats return it again. No filters, limit, idle or replace.', ' history stays read-only; catch --id consumes the selected activity.', ' --now Catch up immediately; bare catch already does this.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Attention:', ' Mentions arrive when you are addressed; bells arrive for everyone; bare says require listen.', ' listen and ignore are future-only and fixed when each say lands.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention Match direct attention for your current participant (and bells).', ' --limit <count> Return one page (default 10, maximum 100).', '', 'Recovery:', ' --replace Replace another active catch for this participant.'],
45
+ names: ['perceive'], usage: '--as <name> perceive [--id <activity-id>] [--now | --idle <duration>] [--from <names>] [--mention] [--limit <count>] [--replace]', usesSquare: true, group: 'participant',
46
+ summary: 'Perceive directed conversation since you last looked.',
47
+ details: ['Modes:', ' --id <activity-id> Perceive exactly one activity addressed to you, with its full body; implies --now.', ' Marks only this activity seen; repeats return it again. No filters, limit, idle or replace.', ' history stays read-only; perceive --id consumes the selected activity.', ' --now Take in activity immediately; bare perceive already does this.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Attention:', ' Mentions arrive when you are addressed; bells arrive for everyone; bare says require listen.', ' listen and ignore are future-only and fixed when each say lands.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention Match direct attention for your current participant (and bells).', ' --limit <count> Return one page (default 10, maximum 100).', '', 'Recovery:', ' --replace Replace another active perceive for this participant.'],
48
48
  },
49
49
  { names: ['done'], usage: '--as <name> done', usesSquare: true, group: 'participant', summary: 'Step out of the square.', details: ['done takes no message. Use express before leaving to send one.'] },
50
50
  {
51
51
  names: ['stream'], usage: 'stream [--ndjson [--for <name>] [--last <N> | --after <id>]]', usesSquare: true, hiddenFromIndex: true,
52
- summary: 'Follow activity without marking it caught.',
52
+ summary: 'Follow activity without marking it perceived.',
53
53
  details: ['Options:', ' --ndjson Emit one JSON event per line.', ' --for <name> With --ndjson, emit notifications for one participant.', ' --last <N> Emit the last N eligible activities first (default 10, maximum 100).', ' --after <id> Resume after a canonical activity id (act/12).'],
54
54
  },
55
55
  {
@@ -64,7 +64,7 @@ const COMMANDS = [
64
64
  { names: ['claude-watch'], usage: '{command}', summary: 'Observe pending attention from a native async Stop hook.', hiddenFromIndex: true },
65
65
  {
66
66
  names: ['history'], usage: 'history [filters] [output]', usesSquare: true, group: 'participant',
67
- summary: 'Read or search the archive without changing what you have caught.',
67
+ summary: 'Read or search the archive without changing what you have perceived.',
68
68
  details: ['Optional --as is accepted but does not change the archive or claim a name.', '', 'Filters:', ' --from <names> Match activities from participants.', ' --since <time> Match activities after a time.', ' --grep <regex> | --fixed <s> Search activity ids, participants, and original bodies.', ' --mention <name> Match direct attention for a participant.', ' --at <ids> Center on comma-separated activity ids; may repeat.', ' -B, -A, -C <N> Set non-negative context around every --at coordinate.', ' --before <id> Read the page immediately before an activity.', ' --after <id> Read the page immediately after an activity.', '', 'Results:', ' --limit <N> Page size (default 10, maximum 100).', ' --order <asc|desc> Set display order (default oldest first).', '', 'Output:', ' --no-truncate --json --format <fields>', ' One result shows its full body; multiple results use previews.', ' --no-truncate shows every original body.'],
69
69
  },
70
70
  { names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
@@ -101,7 +101,7 @@ function isHelpFlag(value) {
101
101
  }
102
102
  export function renderGlobalHelp() {
103
103
  const groups = [
104
- { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'unlisten', 'ignore', 'listening', 'catch', 'history', 'status', 'done'] },
104
+ { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'unlisten', 'ignore', 'listening', 'perceive', 'history', 'status', 'done'] },
105
105
  { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
106
106
  { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor'] },
107
107
  ];
@@ -115,9 +115,9 @@ export function renderGlobalHelp() {
115
115
  ]);
116
116
  return [
117
117
  'Usage: square [--location <square>] [--as <name>] [--owner <id>] <command> [args...]',
118
- 'Environment: SQUARE_LOCATION sets the CLI local artifact path; square-accessing commands require it or --location. Use --as to select a participant.',
119
- 'Identity: --owner overrides SQUARE_OWNER, then a complete harness identity, then user:<OS user>@<host>. Incomplete or ambiguous harness identities refuse. Owners without wake use catch --idle.',
120
- 'Output: --json on join, express, catch, history, listen, ignore, unlisten, listening, done, status, participants and inbox prints the SDK result without observation; a participant handle becomes {name}.',
118
+ 'Location: square-accessing commands require --location <square>. Use square list to find nearby squares and your joined names; use --as to select a participant.',
119
+ 'Identity: --owner overrides SQUARE_OWNER, then a complete harness identity, then user:<OS user>@<host>. Incomplete or ambiguous harness identities refuse. Owners without wake use perceive --idle.',
120
+ 'Output: --json on join, express, perceive, history, listen, ignore, unlisten, listening, done, status, participants and inbox prints the SDK result without observation; a participant handle becomes {name}.',
121
121
  'History emits one JSON document; stream --ndjson emits one SDK record per line.',
122
122
  '',
123
123
  ...commandLines,
package/dist/hints.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** First activity, then every fifth. */
2
2
  export declare function expressHint(ownActivityCount: number): string | undefined;
3
- /** First caught activity, then each time the caught total crosses another ten. */
4
- export declare function catchHint(caughtBefore: number, caughtAfter: number): string | undefined;
3
+ /** First perceived activity, then each time the perceived total crosses another ten. */
4
+ export declare function perceiveHint(perceivedBefore: number, perceivedAfter: number): string | undefined;
package/dist/hints.js CHANGED
@@ -4,9 +4,9 @@ const EXPRESS_HINTS = [
4
4
  "answer someone's actual words — they're standing right there",
5
5
  "say the half-shaped thing — that's what the square is for",
6
6
  ];
7
- const CATCH_HINTS = [
7
+ const PERCEIVE_HINTS = [
8
8
  'want to follow one voice? listen to them; unlisten once you have heard enough',
9
- "nothing to add? quiet is fine — catching is listening too",
9
+ "nothing to add? quiet is fine — perceiving is listening too",
10
10
  "no need to reply just to say you’ve read a message — Square already tracks that",
11
11
  'reply to an activity to reach the person who spoke',
12
12
  ];
@@ -16,10 +16,10 @@ export function expressHint(ownActivityCount) {
16
16
  return undefined;
17
17
  return EXPRESS_HINTS[Math.floor(ownActivityCount / 5) % EXPRESS_HINTS.length];
18
18
  }
19
- /** First caught activity, then each time the caught total crosses another ten. */
20
- export function catchHint(caughtBefore, caughtAfter) {
21
- if (caughtAfter <= caughtBefore)
19
+ /** First perceived activity, then each time the perceived total crosses another ten. */
20
+ export function perceiveHint(perceivedBefore, perceivedAfter) {
21
+ if (perceivedAfter <= perceivedBefore)
22
22
  return undefined;
23
- const crossed = caughtBefore === 0 || Math.floor(caughtAfter / 10) > Math.floor(caughtBefore / 10);
24
- return crossed ? CATCH_HINTS[Math.floor(caughtAfter / 10) % CATCH_HINTS.length] : undefined;
23
+ const crossed = perceivedBefore === 0 || Math.floor(perceivedAfter / 10) > Math.floor(perceivedBefore / 10);
24
+ return crossed ? PERCEIVE_HINTS[Math.floor(perceivedAfter / 10) % PERCEIVE_HINTS.length] : undefined;
25
25
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare } from './square-api.js';
1
+ export { Square, squareLocation, squareClock, discover, inbox, diagnoseSquare } from './square-api.js';
2
2
  export { SquareError, ACTIVITY_METADATA_MAX_BYTES } from './model.js';
3
3
  export { selectOwnSeat, requireOwner, requireJoinName, localUserOwner, harnessOwner, decodeHarnessOwner } from './square-api.js';
4
4
  export { postAndDispatch, dispatchEligibleCards } from './notifications.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare } from './square-api.js';
1
+ export { Square, squareLocation, squareClock, discover, inbox, diagnoseSquare } from './square-api.js';
2
2
  export { SquareError, ACTIVITY_METADATA_MAX_BYTES } from './model.js';
3
3
  export { selectOwnSeat, requireOwner, requireJoinName, localUserOwner, harnessOwner, decodeHarnessOwner } from './square-api.js';
4
4
  export { postAndDispatch, dispatchEligibleCards } from './notifications.js';
package/dist/list.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { DiscoveryResult, DiscoveryOptions, OpenOptions, OperationControl } from './square-facade.js';
1
+ import type { DiscoveryResult, DiscoveryOptions, OperationControl } from './square-facade.js';
2
2
  export declare const DEFAULT_LIST_DEPTH = 4;
3
3
  export declare const MAX_LIST_DEPTH = 16;
4
4
  export declare const DEFAULT_LIST_LIMIT = 20;
5
5
  export declare const MAX_LIST_LIMIT = 100;
6
6
  /** Bounded deterministic discovery for the public resource API. */
7
- export declare function discoverSquares(root: string, options?: DiscoveryOptions, control?: OperationControl, openOptions?: OpenOptions): Promise<DiscoveryResult>;
7
+ export declare function discoverSquares(root: string, options?: DiscoveryOptions, control?: OperationControl): Promise<DiscoveryResult>;
package/dist/list.js CHANGED
@@ -18,7 +18,7 @@ async function readSquareListItem(filePath, root, options, control) {
18
18
  const artifact = createArtifactPort(filePath, undefined, control?.signal);
19
19
  let projection;
20
20
  try {
21
- projection = (await artifact.read(listPresentation, control?.signal)).value;
21
+ projection = (await artifact.read(facts => listPresentation(facts, options.owner), control?.signal)).value;
22
22
  }
23
23
  finally {
24
24
  await artifact.close();
@@ -29,6 +29,7 @@ async function readSquareListItem(filePath, root, options, control) {
29
29
  context: [...projection.context].map((line) => line.trim()).filter(Boolean),
30
30
  participants: [...projection.participants],
31
31
  activities: projection.activities,
32
+ ...(projection.joined === undefined ? {} : { joined: projection.joined }),
32
33
  };
33
34
  }
34
35
  catch (error) {
@@ -38,7 +39,7 @@ async function readSquareListItem(filePath, root, options, control) {
38
39
  }
39
40
  }
40
41
  /** Bounded deterministic discovery for the public resource API. */
41
- export async function discoverSquares(root, options = {}, control, openOptions = {}) {
42
+ export async function discoverSquares(root, options = {}, control) {
42
43
  const depth = options.depth ?? DEFAULT_LIST_DEPTH;
43
44
  const limit = options.limit ?? DEFAULT_LIST_LIMIT;
44
45
  if (!Number.isSafeInteger(depth) || depth < 0 || depth > MAX_LIST_DEPTH
@@ -74,7 +75,7 @@ export async function discoverSquares(root, options = {}, control, openOptions =
74
75
  await walk(fullPath, level + 1);
75
76
  }
76
77
  else if (entry.isFile()) {
77
- const item = await readSquareListItem(fullPath, root, openOptions, control);
78
+ const item = await readSquareListItem(fullPath, root, options, control);
78
79
  if (item !== null)
79
80
  items.push(item);
80
81
  }
package/dist/mcp.d.ts CHANGED
@@ -2,8 +2,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type Owner } from './square-api.js';
3
3
  import type { SquareAtInput } from './square-facade.js';
4
4
  import type { WakeAddress } from './model.js';
5
- /** The server owns its artifact connection lifetime, so callers cannot lend one. */
6
- export interface SquareMcpOptions extends Omit<SquareAtInput, 'resources'> {
5
+ /** Each tool call chooses its square and owns its connection lifetime. */
6
+ export interface SquareMcpOptions extends Omit<SquareAtInput, 'path'> {
7
7
  readonly owner?: Owner;
8
8
  readonly wakeAddress?: WakeAddress;
9
9
  }