@astrosheep/square 0.3.77 → 0.3.79

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 (76) hide show
  1. package/dist/attention-presentation.d.ts +3 -1
  2. package/dist/attention-presentation.js +45 -13
  3. package/dist/automatic-session.js +1 -1
  4. package/dist/boundary-presentation.js +4 -6
  5. package/dist/claude-mod.js +1 -1
  6. package/dist/cli/catch-presentation.d.ts +2 -2
  7. package/dist/cli/catch-presentation.js +3 -3
  8. package/dist/cli/context.d.ts +11 -1
  9. package/dist/cli/context.js +14 -3
  10. package/dist/cli/express-presentation.d.ts +6 -5
  11. package/dist/cli/express-presentation.js +35 -30
  12. package/dist/cli/maintenance-commands.js +8 -8
  13. package/dist/cli/mcp-server-command.js +1 -1
  14. package/dist/cli/observation-commands.d.ts +4 -4
  15. package/dist/cli/observation-commands.js +20 -16
  16. package/dist/cli/program.d.ts +1 -1
  17. package/dist/cli/program.js +13 -7
  18. package/dist/cli/square-commands.js +35 -23
  19. package/dist/decisions.d.ts +9 -4
  20. package/dist/decisions.js +17 -8
  21. package/dist/delivery.d.ts +1 -1
  22. package/dist/help.js +15 -2
  23. package/dist/host-ledger-migration.js +2 -6
  24. package/dist/host-ledger-sqlite.js +2 -2
  25. package/dist/inbox.js +3 -3
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +1 -2
  28. package/dist/ledger-maintenance.d.ts +1 -1
  29. package/dist/ledger-maintenance.js +1 -1
  30. package/dist/list.d.ts +3 -3
  31. package/dist/list.js +1 -1
  32. package/dist/mcp.d.ts +2 -2
  33. package/dist/mcp.js +12 -7
  34. package/dist/mention-parse.d.ts +2 -8
  35. package/dist/mention-parse.js +6 -9
  36. package/dist/model.d.ts +5 -1
  37. package/dist/notifications.js +24 -8
  38. package/dist/participant-host.js +5 -0
  39. package/dist/plugins/opencode/index.js +1 -1
  40. package/dist/square-actions.d.ts +22 -9
  41. package/dist/square-actions.js +27 -19
  42. package/dist/square-api.d.ts +43 -0
  43. package/dist/square-api.js +471 -0
  44. package/dist/square-facade.d.ts +124 -203
  45. package/dist/square-sqlite.d.ts +2 -8
  46. package/dist/square-sqlite.js +3 -11
  47. package/dist/square-tools.d.ts +4 -3
  48. package/dist/square-tools.js +22 -16
  49. package/dist/stream.d.ts +2 -2
  50. package/dist/views.d.ts +16 -12
  51. package/dist/views.js +13 -27
  52. package/guides/application-demand-optimization.md +5 -0
  53. package/guides/application-facade-delivery-evidence.md +6 -1
  54. package/guides/claude-native.md +3 -1
  55. package/guides/mcp-connection-lifetime.md +2 -2
  56. package/guides/mcp.md +1 -1
  57. package/guides/migration.md +2 -1
  58. package/guides/operation-cancellation.md +2 -0
  59. package/guides/primitive-catch-consolidation.md +5 -0
  60. package/guides/primitive-history-consolidation.md +5 -0
  61. package/guides/primitive-status-consolidation.md +5 -0
  62. package/guides/primitive-storage-consolidation.md +5 -0
  63. package/guides/row-storage-design.md +2 -2
  64. package/guides/square-api.md +107 -0
  65. package/package.json +1 -1
  66. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  67. package/plugins/claude/skills/square/SKILL.md +3 -3
  68. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  69. package/plugins/opencode/index.ts +1 -1
  70. package/plugins/paseo/client/markdown.tsx +16 -6
  71. package/plugins/pi/square-pi.js +70 -30
  72. package/skills/square/SKILL.md +3 -3
  73. package/dist/square-application.d.ts +0 -38
  74. package/dist/square-application.js +0 -683
  75. package/guides/application-boundary.md +0 -32
  76. package/guides/application-facade-design.md +0 -98
@@ -1,18 +1,24 @@
1
1
  import { helpRequest } from '../help.js';
2
2
  import { isSquareError } from '../model.js';
3
3
  import { commandPrefix, formatRefusal, refusalText, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, takeoverRecoveryLines, } from '../presentation.js';
4
- import { applicationFor, defaultContext, parseGlobalArgs } from './context.js';
4
+ import { withSquare, defaultContext, parseGlobalArgs } from './context.js';
5
5
  import { executeRegisteredCommand, findCommand } from './registry.js';
6
6
  async function handleSquareError(error, context) {
7
7
  if (!isSquareError(error))
8
8
  throw error;
9
9
  const detail = error.operation;
10
10
  const squarePath = context?.squarePath;
11
- const recovery = context === undefined || squarePath === undefined ? undefined
12
- : await applicationFor({ ...context, name: detail?.participant ?? context.name }).recovery().catch(() => undefined);
13
- const name = detail?.participant ?? recovery?.data.participant ?? context?.name;
11
+ const recovery = error.facts?.reason === 'migration_required' || context === undefined || squarePath === undefined ? undefined
12
+ : await withSquare({ ...context, name: detail?.participant ?? context.name }, (square) => square.recovery(detail?.participant ?? context.name)).catch(() => undefined);
13
+ const name = detail?.participant ?? recovery?.participant ?? context?.name;
14
14
  const existing = [refusalText(error)];
15
- if (squarePath !== undefined && name !== undefined && error.code === 'already_joined') {
15
+ if (error.facts?.reason === 'migration_required') {
16
+ if (error.facts.ledgerRoot !== undefined)
17
+ existing.push('square ledger migrate', 'square ledger verify');
18
+ else if (squarePath !== undefined)
19
+ existing.push(`${commandPrefix(squarePath)} migrate`);
20
+ }
21
+ else if (squarePath !== undefined && name !== undefined && error.code === 'already_joined') {
16
22
  existing.splice(0, 1, `✕ ${participantIdentity(name)} shoos you out of the square`, ` · ${participantIdentity(name)} already stands here — another session holds the name`);
17
23
  existing.push(...takeoverRecoveryLines(squarePath, name));
18
24
  }
@@ -34,11 +40,11 @@ async function handleSquareError(error, context) {
34
40
  process.stderr.write(`${existing.join('\n')}\n`);
35
41
  else
36
42
  process.stderr.write(formatRefusal(squarePath, existing, {
37
- participantCount: recovery?.data.standingCount, held: recovery?.data.held,
43
+ participantCount: recovery?.standingCount, held: recovery?.held,
38
44
  }));
39
45
  process.exit(error.code === 'not_found' ? 1 : 2);
40
46
  }
41
- /** Parse global flags, select an executable adapter, and leave operations to the application. */
47
+ /** Parse global flags, select an adapter, and execute through Square/Participant. */
42
48
  export async function runCli(rawArgs = process.argv.slice(2)) {
43
49
  let context;
44
50
  try {
@@ -1,9 +1,9 @@
1
1
  import { formatHardCap } from '../model.js';
2
2
  import { commandPrefix, participantCommandPrefix, participantIdentity, renderRoomChangeText, renderAmbientEvent, withPathOutput, } from '../presentation.js';
3
- import { formatActivityId } from '../square-application.js';
3
+ import { Square, formatActivityId } from '../square-api.js';
4
4
  import { style } from '../tty-style.js';
5
5
  import { executeExpression } from './express-presentation.js';
6
- import { applicationFor, fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
6
+ import { squareInput, withSquare, fail, parseBoundedLimit, parseHardCap, parsePositiveInteger, readStdin, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
7
7
  const JOIN_DEFAULT_LAST = 10;
8
8
  const JOIN_MAX_LAST = 100;
9
9
  export const buildCommand = {
@@ -42,11 +42,18 @@ export const buildCommand = {
42
42
  const markdown = await readStdin();
43
43
  if (markdown.trim() === '')
44
44
  fail('Missing Markdown body snippet on stdin.');
45
- const { data, projection } = await applicationFor(context).build({ ...intent.options, markdown });
45
+ const square = await Square.build({ ...squareInput(context), ...intent.options, markdown });
46
+ let data;
47
+ try {
48
+ data = await square.status();
49
+ }
50
+ finally {
51
+ await square.close();
52
+ }
46
53
  const cap = data.hardCap === null ? 'unlimited' : formatHardCap(data.hardCap);
47
54
  const throttle = data.throttlePerMinute === undefined ? [] : [` · throttle ${data.throttlePerMinute}/min`];
48
- return withPathOutput(data.location, ['· the square stands ready', ` · cap ${cap}`, ...throttle].join('\n'), {
49
- participantCount: projection.standingCount, held: projection.held,
55
+ return withPathOutput(square.location, ['· the square stands ready', ` · cap ${cap}`, ...throttle].join('\n'), {
56
+ participantCount: 0, held: false,
50
57
  });
51
58
  },
52
59
  present: (result) => process.stdout.write(result),
@@ -68,12 +75,14 @@ export const joinCommand = {
68
75
  return { lastN, kick };
69
76
  },
70
77
  async execute(intent, context) {
71
- const { data, projection: after } = await applicationFor(context).join({ takeover: intent.kick, last: intent.lastN });
72
- const name = data.participant;
73
- const activities = after.recentActivities.map((event) => renderAmbientEvent(event, name, {
78
+ const data = await withSquare(context, (square) => square.join(context.name, { takeover: intent.kick, last: intent.lastN, observe: true }));
79
+ const after = data.observation;
80
+ const entry = after.entry;
81
+ const name = data.participant.name;
82
+ const activities = (entry?.recentActivities ?? []).map((event) => renderAmbientEvent(event, name, {
74
83
  now: after.now, preview: 200,
75
- actNumber: event.kind === 'say' ? after.sayNumbers[event.index] : undefined,
76
- perception: after.perceptions[event.index],
84
+ actNumber: event.kind === 'say' ? entry.sayNumbers[event.index] : undefined,
85
+ perception: entry.perceptions[event.index],
77
86
  })).filter(Boolean).join('\n\n');
78
87
  const fallback = after.automaticDeliveryAvailable ? [] : [
79
88
  '', `${participantCommandPrefix(after.location, name)} catch --idle 30m`,
@@ -83,13 +92,14 @@ export const joinCommand = {
83
92
  : data.kind === 'reconnected' ? '● you are already in the square'
84
93
  : `✓ you banished the original ${participantIdentity(name)} — the name is yours`;
85
94
  const isRejoin = data.kind !== 'joined';
86
- const entryScene = isRejoin ? '' : after.joinedBefore
87
- ? '*the flagstones are where you left them. so is your shadow.*' : after.scene;
95
+ const entryScene = isRejoin || entry === undefined ? '' : entry.joinedBefore
96
+ ? '*the flagstones are where you left them. so is your shadow.*' : entry.scene;
88
97
  return withPathOutput(after.location, [
89
98
  entryLine, '',
90
99
  "· carved into the fountain's edge: every word here lands on a real ear — speak when someone needs it.",
91
100
  ...(entryScene === '' ? [] : ['', entryScene]),
92
- ...(isRejoin || after.joinContext === '' ? [] : ['', style('dim', 'context'), after.joinContext]),
101
+ ...(isRejoin || !entry?.joinContext ? [] : ['', style('dim', 'context'), entry.joinContext]),
102
+ ...(after.enrichmentUnavailable ? [' · joined; recent activity and context are unavailable right now'] : []),
93
103
  ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
94
104
  ...fallback,
95
105
  ].join('\n'), { participantCount: after.standingCount, held: after.held });
@@ -128,10 +138,6 @@ function parseActivity(argv, context) {
128
138
  bodyArgs.push(argument);
129
139
  }
130
140
  const reach = bell ? 'bell' : undefined;
131
- if (bell && (noMention || mentions.length > 0))
132
- fail('✕ --bell cannot be combined with --mention or --no-mention\nsquare express --help');
133
- if (noMention && mentions.length > 0)
134
- fail('✕ --no-mention cannot be combined with --mention\nsquare express --help');
135
141
  if (bodyArgs.length !== 1) {
136
142
  if (bodyArgs.length === 0 && !process.stdin.isTTY)
137
143
  bodyArgs.push('-');
@@ -144,7 +150,7 @@ export const expressCommand = {
144
150
  parse: parseActivity,
145
151
  async execute(intent, context) {
146
152
  const rawInput = await resolveBody(intent.activity);
147
- await executeExpression(applicationFor(context), requireSquarePath(context), context.name, rawInput, {
153
+ await executeExpression((control) => Square.at(squareInput(context), control), requireSquarePath(context), context.name, rawInput, {
148
154
  force: intent.force, noWait: intent.noWait, noMention: intent.noMention,
149
155
  mentions: intent.mentions, reach: intent.reach, reply: intent.reply,
150
156
  });
@@ -160,7 +166,8 @@ function listenerCommand(verb) {
160
166
  return {
161
167
  parse(argv, context) { return parseListener(argv, context, true); },
162
168
  async execute(intent, context) {
163
- const { data, projection } = await applicationFor(context)[verb](intent.target);
169
+ const data = await withSquare(context, async (square) => (await square.participant(context.name))[verb](intent.target, { observe: true }));
170
+ const projection = data.observation;
164
171
  const actor = participantIdentity(projection.participant);
165
172
  const target = participantIdentity(projection.target);
166
173
  const action = data.activity !== null
@@ -178,7 +185,8 @@ export const ignoreCommand = listenerCommand('ignore');
178
185
  export const listeningCommand = {
179
186
  parse(argv, context) { return parseListener(argv, context, false); },
180
187
  async execute(_intent, context) {
181
- const { data, projection } = await applicationFor(context).listening();
188
+ const data = await withSquare(context, async (square) => (await square.participant(context.name)).listening({ observe: true }));
189
+ const projection = data.observation;
182
190
  const body = data.listening.length === 0
183
191
  ? `○ ${participantIdentity(projection.participant)} is not turned toward anyone`
184
192
  : ['listening', ...data.listening.map((target) => ` · ${participantIdentity(target)}`)].join('\n');
@@ -195,7 +203,8 @@ export const doneCommand = {
195
203
  async execute(intent, context) {
196
204
  const body = intent.body === '-' || ((intent.body === undefined || intent.body === '') && !process.stdin.isTTY)
197
205
  ? await readStdin() : intent.body;
198
- const { data, projection } = await applicationFor(context).done(body);
206
+ const data = await withSquare(context, async (square) => (await square.participant(context.name)).done(body, { observe: true }));
207
+ const projection = data.observation;
199
208
  return withPathOutput(projection.location, `○ you stepped out — your footsteps fade · ${data.activity.id}`, {
200
209
  participantCount: projection.standingCount, held: projection.held,
201
210
  });
@@ -209,7 +218,9 @@ export const holdCommand = {
209
218
  return { body: argv[0] };
210
219
  },
211
220
  async execute(intent, context) {
212
- const { projection } = await applicationFor(context).hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
221
+ const body = (await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim();
222
+ const result = await withSquare(context, async (square) => (await square.participant(context.name)).hold(body, { observe: true }));
223
+ const projection = result.observation;
213
224
  return withPathOutput(projection.location, `· ${renderRoomChangeText(projection.activity)} · ${formatActivityId(projection.activity.index)} · just now`, { participantCount: projection.standingCount, held: projection.held });
214
225
  },
215
226
  present: (result) => process.stdout.write(result),
@@ -221,7 +232,8 @@ export const resumeCommand = {
221
232
  return {};
222
233
  },
223
234
  async execute(_intent, context) {
224
- const { projection } = await applicationFor(context).resume();
235
+ const result = await withSquare(context, async (square) => (await square.participant(context.name)).resume({ observe: true }));
236
+ const projection = result.observation;
225
237
  return withPathOutput(projection.location, `✓ ${renderRoomChangeText(projection.activity)} · ${formatActivityId(projection.activity.index)} · just now`, { participantCount: projection.standingCount, held: projection.held });
226
238
  },
227
239
  present: (result) => process.stdout.write(result),
@@ -5,7 +5,7 @@ type TimedAct<K extends Act['kind'] = Act['kind']> = Extract<Act, {
5
5
  }> & {
6
6
  at: number;
7
7
  };
8
- import type { ParticipantSummary as ParticipantStatus, UnreadActivitySummary } from './square-facade.js';
8
+ import type { ExpressOptions, ExpressResult, ParticipantSummary as ParticipantStatus, UnreadActivitySummary } from './square-facade.js';
9
9
  export type { ParticipantSummary as ParticipantStatus, ParticipantPresence as CorePresenceState, UnreadActivitySummary, UnreadActivityPreview } from './square-facade.js';
10
10
  export declare function resolveKnownName(facts: SquareRead, name: string): string;
11
11
  export interface DecideJoinResult {
@@ -20,11 +20,10 @@ export interface DecideImplicitJoinResult {
20
20
  }
21
21
  export declare function decideJoin(facts: SquareRead, name: string, now: number): DecideJoinResult;
22
22
  export declare function decideImplicitJoin(facts: SquareRead, name: string, now: number): DecideImplicitJoinResult;
23
- export type ActDecision = {
23
+ export type ActDecision = ({
24
24
  type: 'sent';
25
25
  act: TimedAct;
26
- replyAuthorNotHere?: string;
27
- } | {
26
+ } & Pick<ExpressResult, 'replyAuthorNotHere' | 'unmatchedMentions'>) | {
28
27
  type: 'blocked';
29
28
  activitySummaries: UnreadActivitySummary[];
30
29
  } | {
@@ -43,6 +42,12 @@ export type ActDecision = {
43
42
  };
44
43
  /** Count the directed suffix, but hydrate only its last three previews per spelling. */
45
44
  export declare function unreadActivitySummaries(facts: SquareRead, name: string, now: number): UnreadActivitySummary[];
45
+ export declare function expressionReach(input: {
46
+ body: string;
47
+ } & Pick<ExpressOptions, 'reach' | 'mentions' | 'noMention'>): {
48
+ bodyMentions: string[];
49
+ hasReachWithoutReply: boolean;
50
+ };
46
51
  export declare function decideAct(facts: SquareRead, input: {
47
52
  name: string;
48
53
  body: string;
package/dist/decisions.js CHANGED
@@ -2,7 +2,7 @@ import { SquareError, isSquareError, sameName, nameKey, validateName, } from './
2
2
  import { participantIdentity } from './participant-identity.js';
3
3
  import { UNREAD_BLOCK_GRACE_MS, freshWatchLease, readCursor, FACT_PAGE_SIZE, THROTTLE_WINDOW_MS } from './runtime.js';
4
4
  import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
5
- import { scanMentionCandidates, resolveNameAt } from './mention-parse.js';
5
+ import { scanMentionCandidates, resolveMention } from './mention-parse.js';
6
6
  export function resolveKnownName(facts, name) {
7
7
  validateName(name);
8
8
  const known = facts.participant(name)?.name;
@@ -84,6 +84,11 @@ export function unreadActivitySummaries(facts, name, now) {
84
84
  previews: summary.previews.map((preview) => ({ number: preview.number, act: byIndex.get(preview.index), perception: 'full' })),
85
85
  })).sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
86
86
  }
87
+ export function expressionReach(input) {
88
+ const bodyMentions = input.reach === 'bell' || input.noMention === true ? [] : scanMentionCandidates(input.body);
89
+ const hasReachWithoutReply = input.reach === 'bell' || (input.mentions?.length ?? 0) > 0 || input.noMention === true || bodyMentions.length > 0;
90
+ return { bodyMentions, hasReachWithoutReply };
91
+ }
87
92
  export function decideAct(facts, input) {
88
93
  const { now, force, body, reach, reply } = input;
89
94
  const current = resolveStanding(facts, input.name, true);
@@ -92,6 +97,8 @@ export function decideAct(facts, input) {
92
97
  throw activityRefusal('invalid_args', 'express body cannot be empty');
93
98
  const roster = facts.roster();
94
99
  const joinedNames = roster.filter((participant) => participant.joined).map((participant) => participant.name);
100
+ if (reach === 'bell' && input.mentions?.length)
101
+ throw activityRefusal('invalid_args', 'A bell cannot be combined with a mention', { reason: 'bell_mention_conflict', targets: [...input.mentions] });
95
102
  const mentionNames = [];
96
103
  const addMention = (resolved) => {
97
104
  if (mentionNames.some((existing) => sameName(existing, resolved)))
@@ -107,21 +114,22 @@ export function decideAct(facts, input) {
107
114
  throw activityRefusal('invalid_args', `${participantIdentity(requested)} is not standing in this square`, { reason: 'not_standing', targets: [requested] });
108
115
  addMention(resolved);
109
116
  }
110
- const bodyMentions = input.noMention === true ? [] : scanMentionCandidates(body);
111
- for (const candidate of bodyMentions) {
112
- const resolved = resolveNameAt(body, candidate, joinedNames);
117
+ const { bodyMentions, hasReachWithoutReply } = expressionReach(input);
118
+ const unmatchedMentions = [];
119
+ for (const token of bodyMentions) {
120
+ const resolved = resolveMention(token, joinedNames);
113
121
  if (resolved !== undefined)
114
122
  addMention(resolved);
123
+ else
124
+ unmatchedMentions.push(token);
115
125
  }
116
- if (reach === 'bell' && mentionNames.length > 0)
117
- throw activityRefusal('invalid_args', 'A bell cannot be combined with a mention', { reason: 'bell_mention_conflict', targets: mentionNames });
118
- if (input.requireExplicitReach && reach !== 'bell' && mentionNames.length === 0 && bodyMentions.length === 0 && reply === undefined && input.noMention !== true) {
126
+ if (input.requireExplicitReach && !hasReachWithoutReply && reply === undefined) {
119
127
  throw activityRefusal('invalid_args', 'Express needs a recipient, reply, bell, or explicit bare reach', { reason: 'explicit_reach_required' });
120
128
  }
121
129
  const header = facts.header();
122
130
  if (reply !== undefined && (!Number.isSafeInteger(reply) || reply < 0 || reply >= header.nextActIndex)) {
123
131
  const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
124
- throw activityRefusal('invalid_args', `Unknown reply activity: ${label}`, { reason: 'invalid_reply', activityId: label, activityExists: false });
132
+ throw activityRefusal('invalid_args', `Unknown reply activity: ${label}`, { reason: 'invalid_reply', activityId: label, activityExists: false, hasReachWithoutReply });
125
133
  }
126
134
  const replyAuthor = reply === undefined ? undefined : facts.activity(reply)?.actor;
127
135
  const replyAuthorNotHere = replyAuthor === undefined || sameName(replyAuthor, name) || facts.participant(replyAuthor)?.joined ? undefined : replyAuthor;
@@ -152,6 +160,7 @@ export function decideAct(facts, input) {
152
160
  }
153
161
  return { type: 'sent', act: { kind: 'say', actor: name, at: now, body, mentions: mentionNames,
154
162
  ...(reach === undefined ? {} : { reach }), ...(reply === undefined ? {} : { reply }) },
163
+ ...(unmatchedMentions.length === 0 ? {} : { unmatchedMentions }),
155
164
  ...(replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere }) };
156
165
  }
157
166
  export function validateDoneBody(body) {
@@ -47,7 +47,7 @@ export declare function pendingPages(artifact: SquareArtifactPort, scan: Pending
47
47
  at: number;
48
48
  actor: string;
49
49
  recipient: string;
50
- route: "bell" | "mention" | "reply" | "attention";
50
+ route: "bell" | "reply" | "mention" | "attention";
51
51
  }[], void, unknown>;
52
52
  /** A pass retains only its captured joins; routes and individual receipts are fresh. */
53
53
  export declare function pendingFacts(facts: SquareRead, scan: PendingScan, heads: readonly DirectedHead[]): {
package/dist/help.js CHANGED
@@ -17,9 +17,22 @@ const COMMANDS = [
17
17
  details: ['Options:', ' --last <N> Show the last N public activities (default 10, maximum 100).', ' --kick End the standing participant and reclaim the name for this session.'],
18
18
  },
19
19
  {
20
- names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] (--mention <name>... | --no-mention | --bell | --reply <activity-id>) <activity | ->', usesSquare: true, group: 'participant',
20
+ names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--mention <name>... | --bell] [--no-mention] [--reply <activity-id>] <activity | ->', usesSquare: true, group: 'participant',
21
21
  summary: 'Speak, gesture, or do both.',
22
- details: ['Reach:', ' --mention <name> Address one participant; repeat for multiple participants. The body stays unchanged.', ' --no-mention Land a bare activity explicitly; @names stay plain text. listen opts someone into future bare delivery.', " --bell Call every participant's attention to this activity.", ' @name An @name in the body also addresses that standing participant; `backticks` keep @text literal.', '', 'Options:', ' -f, --force Express without first catching unread activity or attention etiquette.', ' --no-wait If held or throttled, save a draft and return instead of waiting. A held wait gives up on its own after 10 minutes.', ' --reply <activity-id> Mark this activity as a reply to an earlier activity (for example act/12); it reaches that activity\'s author.'],
22
+ details: [
23
+ 'Reach:',
24
+ ' --mention <name> Address one standing participant; repeat to address more.',
25
+ ' @name Also address a standing participant named in the body; `backticks` keep @text literal.',
26
+ " --bell Call everyone; body @names stay literal. Cannot combine with --mention.",
27
+ ' --no-mention Keep body @names literal; explicit --mention and --reply still apply.',
28
+ ' --reply <activity-id> Also reach the author of an earlier activity (for example act/12).',
29
+ ' Choose reach with a flag above or an @name in the body. Unmatched @names stay literal, with a notice.',
30
+ ' With no mention, bell, or reply, the activity lands bare; only listeners receive it.',
31
+ '',
32
+ 'Options:',
33
+ ' -f, --force Express without first catching unread activity or satisfying attention etiquette.',
34
+ ' --no-wait If held or throttled, save a draft and return instead of waiting. A held wait gives up on its own after 10 minutes.',
35
+ ],
23
36
  },
24
37
  { names: ['listen'], usage: '--as <name> listen <participant>', usesSquare: true, group: 'participant', summary: 'Turn an ear toward one participant\'s future bare says.' },
25
38
  { names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Ignore one participant\'s future mentions and bare says.' },
@@ -5,7 +5,7 @@ import { DatabaseSync } from 'node:sqlite';
5
5
  import { canonicalPathSync } from './canonical-path.js';
6
6
  import { withFileLock } from './file-lock.js';
7
7
  import { hostLedgerRoot } from './host-ledger-root.js';
8
- import { LEDGER_FILENAME, initializeLedger, insertLedgerRecord, ledgerRecordColumns, setLedgerRevisions, upgradeLedger, validateLedger, verifyLedger, inspectLedgerIdentity, } from './host-ledger-sqlite.js';
8
+ import { LEDGER_FILENAME, initializeLedger, insertLedgerRecord, ledgerRecordColumns, setLedgerRevisions, upgradeLedger, validateLedger, verifyLedger, inspectLedgerIdentity, migrationRequired, } from './host-ledger-sqlite.js';
9
9
  import { inSqliteTransaction } from './sqlite-transaction.js';
10
10
  function rootFor(options) {
11
11
  return canonicalPathSync(options.rootPath ?? hostLedgerRoot(options.env));
@@ -25,10 +25,6 @@ async function exists(file) {
25
25
  throw error;
26
26
  }
27
27
  }
28
- function migrationGuidance(root, file) {
29
- const name = file ?? path.join(root, LEDGER_FILENAME);
30
- return `Host ledger at ${root} has a supported older format (${name}).\nStop agent sessions and MCP services using Square with this ledger; keep them stopped until migration and verification succeed; then start them with the updated Square version.\nsquare ledger migrate\nsquare ledger verify`;
31
- }
32
28
  function verificationFailure(root, file, format) {
33
29
  const name = file ?? path.join(root, LEDGER_FILENAME);
34
30
  const detail = format === 'unsupported'
@@ -62,7 +58,7 @@ async function verifyDatabase(file, root, signal) {
62
58
  db = undefined;
63
59
  }
64
60
  if (format === 'old-sqlite')
65
- throw new Error(migrationGuidance(root, file));
61
+ throw migrationRequired(root);
66
62
  throw verificationFailure(root, file, format);
67
63
  }
68
64
  finally {
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
- import { nameKey } from './model.js';
6
+ import { SquareError, nameKey } from './model.js';
7
7
  import { inSqliteTransaction, retrySqliteBusy } from './sqlite-transaction.js';
8
8
  export const LEDGER_FILENAME = 'host-ledger.sqlite';
9
9
  export const LEDGER_APPLICATION_ID = 0x5351484c;
@@ -175,7 +175,7 @@ catch (error) {
175
175
  throw error;
176
176
  } }
177
177
  export function migrationRequired(root) {
178
- return new Error(`Host ledger legacy format needs offline migration at ${root}.\nStop agent sessions and MCP services using Square with this ledger; keep them stopped until migration and verification succeed; then start them with the updated Square version.\nRun square ledger migrate from the same environment, then square ledger verify.`);
178
+ return new SquareError('invalid_args', `Host ledger at ${root} needs offline migration.\nStop sessions and MCP services using it, back it up, then migrate and verify from the same environment before restarting.`, { reason: 'migration_required', ledgerRoot: root });
179
179
  }
180
180
  /** Compact runtime framing. Full schema, projections and integrity belong to explicit verify. */
181
181
  function validateLedgerHeader(db, version = LEDGER_VERSION) {
package/dist/inbox.js CHANGED
@@ -5,7 +5,7 @@ import { canonicalPath } from './canonical-path.js';
5
5
  import { projectBoundaryEligibility, projectSessionBindings } from './square-projections.js';
6
6
  import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
7
7
  import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
8
- import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
8
+ import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
9
9
  function bindingCoordinate(binding) {
10
10
  return JSON.stringify([binding.location, nameKey(binding.participant), binding.sessionId, binding.channel, binding.epoch, binding.cancelledThrough ?? -1]);
11
11
  }
@@ -37,8 +37,8 @@ export function nativePendingPreview(facts, input) {
37
37
  const item = facts.activity(input.actIndex);
38
38
  if (item?.kind !== 'say')
39
39
  return undefined;
40
- return { payload: renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
41
- actor: item.actor, route: head.route, body: item.body }), clipped: attentionBodyIsClipped(item.body) };
40
+ return { payload: renderSquareReminder([renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
41
+ actor: item.actor, route: head.route, body: item.body })]), clipped: attentionBodyIsClipped(item.body) };
42
42
  }
43
43
  async function readInboxMembership(binding, signal) {
44
44
  const artifact = createArtifactPort(binding.location, undefined, signal);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- export { Square } from './square-application.js';
2
- export { createSquareApplication } from './square-application.js';
1
+ export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare, migrateSquare } from './square-api.js';
3
2
  export { SquareError } from './model.js';
4
3
  export { squareAssignedParticipantName } from './registry.js';
5
4
  export { createDefaultWakeTransport } from './notifications.js';
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
- export { Square } from './square-application.js';
2
- export { createSquareApplication } from './square-application.js';
1
+ export { Square, squareLocation, squareClock, createSquareResourceOwner, discover, inbox, diagnoseSquare, migrateSquare } from './square-api.js';
3
2
  export { SquareError } from './model.js';
4
3
  export { squareAssignedParticipantName } from './registry.js';
5
4
  export { createDefaultWakeTransport } from './notifications.js';
@@ -22,7 +22,7 @@ export interface LedgerMaintenanceReport {
22
22
  /** Canonical locations whose artifact could not be read; their rows were retained. */
23
23
  readonly unavailableArtifacts: readonly string[];
24
24
  }
25
- /** Command/application entry: root selection uses the caller's captured environment. */
25
+ /** Maintenance entry: root selection uses the caller's captured environment. */
26
26
  export declare function gcHostLedger(options?: LedgerMaintenanceOptions & {
27
27
  readonly env?: NodeJS.ProcessEnv;
28
28
  }): Promise<LedgerMaintenanceReport>;
@@ -62,7 +62,7 @@ async function provenClosable(location, group) {
62
62
  await artifact.close();
63
63
  }
64
64
  }
65
- /** Command/application entry: root selection uses the caller's captured environment. */
65
+ /** Maintenance entry: root selection uses the caller's captured environment. */
66
66
  export function gcHostLedger(options = {}) {
67
67
  return maintainHostLedger(hostLedgerForEnv(options.env ?? process.env), options);
68
68
  }
package/dist/list.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { DiscoveryApplicationData, DiscoveryApplicationOptions, OpenOptions, OperationControl } from './square-facade.js';
1
+ import type { DiscoveryResult, DiscoveryOptions, OpenOptions, 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
- /** Bounded deterministic discovery. Only the application calls this private operation. */
7
- export declare function discoverSquares(root: string, options?: DiscoveryApplicationOptions, control?: OperationControl, openOptions?: OpenOptions): Promise<DiscoveryApplicationData>;
6
+ /** Bounded deterministic discovery for the public resource API. */
7
+ export declare function discoverSquares(root: string, options?: DiscoveryOptions, control?: OperationControl, openOptions?: OpenOptions): Promise<DiscoveryResult>;
package/dist/list.js CHANGED
@@ -32,7 +32,7 @@ async function readSquareListItem(filePath, root, options, control) {
32
32
  return null;
33
33
  }
34
34
  }
35
- /** Bounded deterministic discovery. Only the application calls this private operation. */
35
+ /** Bounded deterministic discovery for the public resource API. */
36
36
  export async function discoverSquares(root, options = {}, control, openOptions = {}) {
37
37
  const depth = options.depth ?? DEFAULT_LIST_DEPTH;
38
38
  const limit = options.limit ?? DEFAULT_LIST_LIMIT;
package/dist/mcp.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { SquareApplicationContext } from './square-facade.js';
2
+ import type { SquareAtInput } from './square-facade.js';
3
3
  /** The server owns its artifact connection lifetime, so callers cannot lend one. */
4
- export interface SquareMcpOptions extends Omit<SquareApplicationContext, 'resources'> {
4
+ export interface SquareMcpOptions extends Omit<SquareAtInput, 'resources'> {
5
5
  }
6
6
  export type SquareMcpServer = McpServer;
7
7
  export declare function createSquareMcpServer(options: SquareMcpOptions): SquareMcpServer;
package/dist/mcp.js CHANGED
@@ -4,9 +4,9 @@ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from './squ
4
4
  import { operationEnv } from './automatic-session.js';
5
5
  import { readClaudeMcpCall } from './claude-mcp-context.js';
6
6
  import { SQUARE_IDENTITY } from './identity.js';
7
- import { createSquareApplication, createSquareResourceOwner } from './square-application.js';
7
+ import { Square, createSquareResourceOwner } from './square-api.js';
8
8
  export function createSquareMcpServer(options) {
9
- const baseEnv = { ...options.env };
9
+ const baseEnv = { ...(options.env ?? process.env) };
10
10
  const resources = createSquareResourceOwner();
11
11
  // Calls to the configured square borrow one server-owned connection.
12
12
  const inFlight = new Set();
@@ -33,15 +33,20 @@ export function createSquareMcpServer(options) {
33
33
  : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
34
34
  ? operationEnv('opencode', opencodeSessionId, baseEnv)
35
35
  : baseEnv;
36
- const app = createSquareApplication({
36
+ const square = await Square.at({
37
37
  ...options, env, cwd: claude?.cwd ?? options.cwd,
38
- squarePath: location ?? options.squarePath,
38
+ path: location ?? options.path,
39
39
  participant: participant ?? options.participant,
40
40
  // The server owner is bound to its configured square; overrides close after each call.
41
41
  resources: location === undefined && claude === undefined ? resources : undefined,
42
- }, { presentation: false });
43
- const outcome = await tool.run(app, args, { signal: extra.signal });
44
- const structuredContent = outcome.data;
42
+ }, { signal: extra.signal });
43
+ let structuredContent;
44
+ try {
45
+ structuredContent = await tool.run(square, args, { signal: extra.signal });
46
+ }
47
+ finally {
48
+ await square.close();
49
+ }
45
50
  return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent };
46
51
  }
47
52
  catch (error) {
@@ -1,8 +1,2 @@
1
- export interface MentionCandidate {
2
- /** The maximal name-grammar token after `@` (roster-free; resolution may shorten it). */
3
- readonly token: string;
4
- /** UTF-16 index of the `@` in the body. */
5
- readonly index: number;
6
- }
7
- export declare function scanMentionCandidates(body: string): MentionCandidate[];
8
- export declare function resolveNameAt(body: string, candidate: MentionCandidate, names: readonly string[]): string | undefined;
1
+ export declare function scanMentionCandidates(body: string): string[];
2
+ export declare function resolveMention(text: string, names: readonly string[]): string | undefined;
@@ -10,8 +10,8 @@ import { NAME_GRAPHEME_SOURCE, sameName } from './model.js';
10
10
  * (`user@host`), URLs (`x.com/@user`), and `snake_@case` stay literal. Only ASCII
11
11
  * word characters, `_`, `-`, `/`, and `@` block a boundary: CJK adjacency like
12
12
  * `告诉@rei` stays mentionable because CJK text has no spaces to lean on.
13
- * - The token must be a complete name (slash-separated segments allowed); resolution
14
- * never backs off to a shorter prefix, so `@rei2` does not mention `rei`.
13
+ * - Slash-separated names are allowed. Resolution may stop at a CJK suffix, but never
14
+ * at an ASCII handle continuation: `@rei这个` can reach `rei`; `@rei2` cannot.
15
15
  */
16
16
  const MENTION_TOKEN = new RegExp(`@((?:${NAME_GRAPHEME_SOURCE})+(?:/(?:${NAME_GRAPHEME_SOURCE})+)*)`, 'vg');
17
17
  const BOUNDARY_BLOCKER = /[A-Za-z0-9_\-/@]/;
@@ -62,29 +62,26 @@ export function scanMentionCandidates(body) {
62
62
  const start = match.index;
63
63
  if (spans.some(([from, to]) => start >= from && start < to))
64
64
  continue;
65
- const unit = start <= 0 ? undefined : body.charCodeAt(start - 1);
66
- const previous = unit === undefined ? undefined
67
- : unit >= 0xdc00 && unit <= 0xdfff && start >= 2 ? String.fromCodePoint(body.codePointAt(start - 2)) : String.fromCodePoint(unit);
65
+ const previous = body[start - 1];
68
66
  if (previous !== undefined && BOUNDARY_BLOCKER.test(previous))
69
67
  continue;
70
68
  const token = match[1];
71
69
  const key = token.toLocaleLowerCase();
72
70
  if (!seen.has(key)) {
73
71
  seen.add(key);
74
- found.push({ token, index: start });
72
+ found.push(token);
75
73
  }
76
74
  }
77
75
  return found;
78
76
  }
79
77
  /**
80
- * Resolve the longest standing name starting at one candidate's position. A partial
78
+ * Resolve the longest standing name at the start of a candidate token. A partial
81
79
  * match is only accepted when the character right after it is not handle-continuation
82
80
  * (ASCII word char, `_`, `-`, `/`): `@rei2` never degrades into mentioning `rei`, while
83
81
  * CJK-fluent text like `告诉@rei这个` still mentions `rei`.
84
82
  */
85
83
  const TRAILING_BLOCKER = /[A-Za-z0-9_\-/]/;
86
- export function resolveNameAt(body, candidate, names) {
87
- const text = body.slice(candidate.index + 1, candidate.index + 1 + candidate.token.length);
84
+ export function resolveMention(text, names) {
88
85
  let best;
89
86
  for (const name of names) {
90
87
  if (name.length > text.length)
package/dist/model.d.ts CHANGED
@@ -24,10 +24,14 @@ export interface SquareErrorFacts {
24
24
  reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'explicit_reach_required' | 'invalid_reply' | 'invalid_done_body';
25
25
  /** The activity id a `catch --id` refusal named. */
26
26
  activityId?: string;
27
+ /** The host ledger requiring offline migration, rather than the selected artifact. */
28
+ ledgerRoot?: string;
27
29
  /** Requested recipient spellings, without parsing presentation copy. */
28
30
  targets?: readonly string[];
29
31
  /** Whether the artifact holds that activity at all; an unknown id needs an ordinary page. */
30
32
  activityExists?: boolean;
33
+ /** Express has a recipient, bell, body candidate, or explicit bare reach without its reply. */
34
+ hasReachWithoutReply?: boolean;
31
35
  pending?: number;
32
36
  holder?: string;
33
37
  holdReason?: string;
@@ -36,7 +40,7 @@ export interface SquareErrorFacts {
36
40
  declare class SquareErrorBase<Code extends string> extends Error {
37
41
  code: Code;
38
42
  facts?: SquareErrorFacts | undefined;
39
- operation?: import('./square-facade.js').ApplicationFailureDetail;
43
+ operation?: import('./square-facade.js').OperationFailureDetail;
40
44
  constructor(code: Code, message: string, facts?: SquareErrorFacts | undefined);
41
45
  }
42
46
  export declare class SquareError extends SquareErrorBase<SquareErrorCode> {