@astrosheep/square 0.3.77 → 0.3.78

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.
@@ -11,7 +11,7 @@ export interface AttentionPreview {
11
11
  export declare function previewAttentionBody(body: string): string;
12
12
  export declare function attentionBodyIsClipped(body: string): boolean;
13
13
  export declare function displayAttentionPath(squarePath: string): string;
14
- export declare function renderAttentionDescription({ actor, recipient, route }: Pick<AttentionPreview, 'actor' | 'recipient' | 'route'>): string;
14
+ export declare function renderAttentionDescription({ actor, route }: Pick<AttentionPreview, 'actor' | 'route'>): string;
15
15
  /** Full notification text for human-facing expansion, never the bounded model preview. */
16
16
  export declare function renderAttentionFull(attention: AttentionPreview): string;
17
17
  export declare function renderAttentionPreview(attention: AttentionPreview): string;
@@ -1,6 +1,5 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { formatActivityId } from './square-core.js';
3
- import { participantCommandPrefix } from './presentation.js';
4
3
  export const ATTENTION_BODY_MAX = 200;
5
4
  export function previewAttentionBody(body) {
6
5
  const compact = body.replace(/\r\n/g, '\n');
@@ -16,12 +15,12 @@ export function displayAttentionPath(squarePath) {
16
15
  ? `~${squarePath.slice(homedir().length)}`
17
16
  : squarePath;
18
17
  }
19
- export function renderAttentionDescription({ actor, recipient, route }) {
18
+ export function renderAttentionDescription({ actor, route }) {
20
19
  switch (route) {
21
- case 'mention': return `${actor} spoke your name (${recipient})`;
22
- case 'reply': return `${actor} answered you (${recipient})`;
23
- case 'attention': return `${actor} spoke · you’re listening to ${actor}`;
24
- case 'bell': return `${actor} rang the bell · everyone’s attention`;
20
+ case 'mention': return `${actor} spoke your name`;
21
+ case 'reply': return `${actor} answered you`;
22
+ case 'attention': return `${actor} spoke · you’re listening`;
23
+ case 'bell': return `${actor} rang the bell`;
25
24
  }
26
25
  }
27
26
  /** Full notification text for human-facing expansion, never the bounded model preview. */
@@ -31,7 +30,7 @@ export function renderAttentionFull(attention) {
31
30
  function renderAttentionBlock(attention, body) {
32
31
  return [
33
32
  '<square-activity>',
34
- `· ${displayAttentionPath(attention.squarePath)} · ${formatActivityId(attention.actIndex)}`,
33
+ `· ${displayAttentionPath(attention.squarePath)} · ${attention.recipient} · ${formatActivityId(attention.actIndex)}`,
35
34
  `● ${renderAttentionDescription(attention)}`,
36
35
  '',
37
36
  body,
@@ -41,10 +40,8 @@ function renderAttentionBlock(attention, body) {
41
40
  export function renderAttentionPreview(attention) {
42
41
  return [
43
42
  renderAttentionBlock(attention, previewAttentionBody(attention.body)),
44
- // A clipped body ends with the full command to read it all: a labeled caption, then the
45
- // command bare on its own line so it can be copied and run as it is.
46
43
  ...(attentionBodyIsClipped(attention.body)
47
- ? ['· clipped — read it all:', `${participantCommandPrefix(attention.squarePath, attention.recipient)} catch --id ${formatActivityId(attention.actIndex)}`]
44
+ ? ['· clipped — catch it to read the rest']
48
45
  : []),
49
46
  ].join('\n');
50
47
  }
@@ -46,6 +46,7 @@ function renderBoundary(inbox) {
46
46
  const blocks = [];
47
47
  const complete = [];
48
48
  let omitted = 0;
49
+ const omission = (count) => `… ${count} more — catch up to read them`;
49
50
  for (const [index, entry] of queued.entries()) {
50
51
  const { membership, notification } = entry;
51
52
  const block = [
@@ -59,7 +60,7 @@ function renderBoundary(inbox) {
59
60
  }),
60
61
  ].join('\n');
61
62
  const omittedAfter = omitted + queued.length - index - 1;
62
- const prospective = frameBoundary([...blocks, block, ...(omittedAfter > 0 ? [`… ${omittedAfter} ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted.`] : [])]);
63
+ const prospective = frameBoundary([...blocks, block, ...(omittedAfter > 0 ? [omission(omittedAfter)] : [])]);
63
64
  if (prospective.length > CONTEXT_MAX) {
64
65
  omitted += 1;
65
66
  continue;
@@ -72,7 +73,7 @@ function renderBoundary(inbox) {
72
73
  });
73
74
  }
74
75
  return {
75
- context: frameBoundary([...blocks, ...(omitted > 0 ? [`… ${omitted} ${omitted === 1 ? 'notification' : 'notifications'} omitted.`] : [])]),
76
+ context: frameBoundary([...blocks, ...(omitted > 0 ? [omission(omitted)] : [])]),
76
77
  complete,
77
78
  };
78
79
  }
@@ -1,5 +1,5 @@
1
1
  import { createSquareApplication } from '../square-application.js';
2
- import { renderDoctorClean, renderDoctorUnfixable, withPathOutput } from '../presentation.js';
2
+ import { commandPrefix, renderDoctorClean, renderDoctorUnfixable, withPathOutput } from '../presentation.js';
3
3
  import { requireSquarePath, usage } from './context.js';
4
4
  export const migrateCommand = {
5
5
  parse(argv, context) {
@@ -32,7 +32,7 @@ export const doctorCommand = {
32
32
  const squarePath = requireSquarePath(context);
33
33
  const { data: diagnosis, projection } = await createSquareApplication({ cwd: context.cwd, env: context.env, squarePath }).diagnose();
34
34
  if (diagnosis.migrationRequired !== undefined) {
35
- return { output: withPathOutput(squarePath, `${diagnosis.migrationRequired}\n`), exitCode: 2 };
35
+ return { output: withPathOutput(squarePath, `${diagnosis.migrationRequired}\n${commandPrefix(squarePath)} migrate\n`), exitCode: 2 };
36
36
  }
37
37
  if (diagnosis.unfixable !== undefined || diagnosis.standingCount === undefined) {
38
38
  return {
@@ -8,11 +8,17 @@ async function handleSquareError(error, context) {
8
8
  throw error;
9
9
  const detail = error.operation;
10
10
  const squarePath = context?.squarePath;
11
- const recovery = context === undefined || squarePath === undefined ? undefined
11
+ const recovery = error.facts?.reason === 'migration_required' || context === undefined || squarePath === undefined ? undefined
12
12
  : await applicationFor({ ...context, name: detail?.participant ?? context.name }).recovery().catch(() => undefined);
13
13
  const name = detail?.participant ?? recovery?.data.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
  }
@@ -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/model.d.ts CHANGED
@@ -24,6 +24,8 @@ 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. */
@@ -25,17 +25,11 @@ export interface LegacyFraming {
25
25
  readonly userVersion: number;
26
26
  readonly journalMode: string;
27
27
  }
28
- /**
29
- * The one recovery command for an owned legacy artifact, with a shell-quoted
30
- * path. Kept beside framing recognition so the refusal and migration
31
- * instruction cannot drift apart. This codec stays free of the renderer.
32
- */
33
- export declare function migrationRecoveryCommand(location: string): string;
34
28
  /**
35
29
  * Read-only legacy framing: application id, format version, journal mode and the
36
30
  * exact owned snapshot schema. It decodes no snapshot payload, writes no pragma
37
- * and runs no DDL, so ordinary open can establish ownership before offering
38
- * the migration command. Anything else is unrelated or unsupported, not repaired.
31
+ * and runs no DDL, so ordinary open can establish ownership before requiring
32
+ * migration. Anything else is unrelated or unsupported, not repaired.
39
33
  */
40
34
  export declare function validateLegacyFraming(database: DatabaseSync, applicationId: number, location?: string): LegacyFraming;
41
35
  export declare const SCHEMA_SQL = "CREATE TABLE square_meta (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),\n next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),\n hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),\n throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),\n preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),\n warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))\n);\nCREATE TABLE activities (\n act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('join','done','say','hold','resume','read','listen','ignore')),\n actor_json TEXT COLLATE BINARY,\n actor_key_json TEXT COLLATE BINARY,\n reach TEXT CHECK (reach = 'bell'),\n reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),\n CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))\n);\nCREATE INDEX activity_kind_index ON activities(kind, act_index);\nCREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);\nCREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';\nCREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';\nCREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';\n\nCREATE TABLE receipts (\n owner_json TEXT COLLATE BINARY NOT NULL,\n act_index INTEGER NOT NULL CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n PRIMARY KEY (owner_json, act_index)\n) WITHOUT ROWID;\nCREATE INDEX receipt_owner_time ON receipts(owner_json, at);\nCREATE TABLE catch_leases (\n owner_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,\n lease_id_json TEXT NOT NULL,\n heartbeat_at REAL NOT NULL,\n expires_at REAL NOT NULL CHECK (expires_at >= heartbeat_at),\n filter_json TEXT CHECK (filter_json IS NULL OR json_valid(filter_json))\n) WITHOUT ROWID;\n\nCREATE TABLE routes (\n position INTEGER PRIMARY KEY CHECK (position >= 0),\n participant_key_json TEXT COLLATE BINARY NOT NULL,\n session_json TEXT COLLATE BINARY NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('opencode-server','codex-queue','claude-native','paseo')),\n updated_at REAL NOT NULL,\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json))\n);\nCREATE INDEX route_participant ON routes(participant_key_json, position);\nCREATE INDEX route_session ON routes(session_json, position);\n\nCREATE TABLE participants (\n name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,\n name_json TEXT NOT NULL,\n first_index INTEGER NOT NULL UNIQUE,\n joined INTEGER NOT NULL CHECK (joined IN (0,1)),\n done INTEGER NOT NULL CHECK (done IN (0,1)),\n say_count INTEGER NOT NULL CHECK (say_count >= 0),\n last_active_at REAL,\n last_join_index INTEGER,\n CHECK (NOT (joined=1 AND done=1))\n) WITHOUT ROWID;\nCREATE TABLE attention (\n listener_key_json TEXT COLLATE BINARY NOT NULL,\n target_key_json TEXT COLLATE BINARY NOT NULL,\n target_json TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('listen','ignore')),\n since_index INTEGER NOT NULL,\n PRIMARY KEY (listener_key_json, target_key_json)\n) WITHOUT ROWID;\nCREATE INDEX attention_target ON attention(target_key_json, kind, listener_key_json);\nCREATE TABLE landed_audience (\n act_index INTEGER NOT NULL,\n recipient_key_json TEXT COLLATE BINARY NOT NULL,\n recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),\n route TEXT NOT NULL CHECK (route IN ('mention','reply','attention','bell')),\n PRIMARY KEY (act_index, recipient_key_json),\n UNIQUE (act_index, recipient_order)\n) WITHOUT ROWID;\nCREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);";
@@ -13,19 +13,11 @@ const V1_SNAPSHOT_COLUMNS = [
13
13
  { cid: 1, name: 'revision', type: 'INTEGER', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
14
14
  { cid: 2, name: 'state', type: 'TEXT', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
15
15
  ];
16
- /**
17
- * The one recovery command for an owned legacy artifact, with a shell-quoted
18
- * path. Kept beside framing recognition so the refusal and migration
19
- * instruction cannot drift apart. This codec stays free of the renderer.
20
- */
21
- export function migrationRecoveryCommand(location) {
22
- return `square --location '${location.replace(/'/g, `'\\''`)}' migrate`;
23
- }
24
16
  /**
25
17
  * Read-only legacy framing: application id, format version, journal mode and the
26
18
  * exact owned snapshot schema. It decodes no snapshot payload, writes no pragma
27
- * and runs no DDL, so ordinary open can establish ownership before offering
28
- * the migration command. Anything else is unrelated or unsupported, not repaired.
19
+ * and runs no DDL, so ordinary open can establish ownership before requiring
20
+ * migration. Anything else is unrelated or unsupported, not repaired.
29
21
  */
30
22
  export function validateLegacyFraming(database, applicationId, location = '') {
31
23
  const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
@@ -328,7 +320,7 @@ export function validateSqliteHeader(db, location = '') {
328
320
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
329
321
  if (identity.user_version === 1) {
330
322
  validateLegacyFraming(db, APPLICATION_ID, location);
331
- throw new SquareError('invalid_args', `Square artifact needs migration.\n${migrationRecoveryCommand(location)}`, { reason: 'migration_required' });
323
+ throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
332
324
  }
333
325
  requireFact(identity.user_version === 2, 'unsupported SQLite format version.');
334
326
  const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { isSquareError } from './model.js';
3
3
  const location = z.string().optional().describe('Path to the .square artifact, relative to the current working directory. Defaults to the configured square.');
4
- const participant = z.string().optional().describe('Participant name, same as --as. Defaults to the configured participant or the current session’s unambiguous participant.');
4
+ const participant = z.string().optional().describe('Participant name. Defaults to the configured participant or the current session’s unambiguous participant.');
5
5
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
6
6
  const stringArray = z.array(z.string());
7
7
  function tool(name, description, shape, run) {
@@ -75,7 +75,9 @@ old ends cannot retire that replacement. Unchanged reload preserves epochs.
75
75
  ## Body, admission, and presentation
76
76
 
77
77
  The coordinator uses Square's canonical audience projection and bounded body
78
- preview. A clipped preview ends with the full `catch --id act/<index>` command.
78
+ preview. Each notification identifies the square, receiving participant and
79
+ activity before its event and body. A clipped preview describes catching that
80
+ activity to read it in full; it never embeds shell commands or tool-call syntax.
79
81
  Opaque text correlation resolves through bound memberships and the existing
80
82
  attempt ledger, never a path or asserted source from incoming text.
81
83
 
@@ -6,7 +6,8 @@ sparse receipts, catch leases, ordered routes and the small configuration
6
6
  facts — in one in-place database.
7
7
 
8
8
  Existing artifacts are **not** converted automatically. A normal read of a
9
- version 1 artifact refuses with the exact command to run:
9
+ version 1 artifact refuses with a descriptive offline-migration requirement.
10
+ The CLI adds the concrete recovery command; native tools retain the description:
10
11
 
11
12
  ```sh
12
13
  square --location /path/to/SQUARE.square migrate
@@ -465,7 +465,7 @@ These terms apply together with the domain specification above.
465
465
  ## Migration protocol
466
466
 
467
467
  - Only `square --location <path.square> migrate`. No --check, --backup, --force, auto-migrate, bulk mode, separate lock database, sidecar, daemon, or runtime dual reader. An operator can make an offline backup with ordinary tooling; the command does not invent a backup lifecycle.
468
- - application_id remains 0x53515245; user_version 2 identifies rows. Normal new-format reads/writes refuse version 1 with invalid_args / reason migration_required and a shell-quoted full migration command. build --force does not bypass it. Unknown/unrelated/invalid formats are refused, not repaired.
468
+ - application_id remains 0x53515245; user_version 2 identifies rows. Normal new-format reads/writes refuse version 1 with invalid_args / reason migration_required and descriptive offline-migration guidance. The CLI adds the shell-quoted recovery command; native tool errors do not embed invocation syntax. build --force does not bypass it. Unknown/unrelated/invalid formats are refused, not repaired.
469
469
  - `doctor` remains read-only: recognizes legacy v1 and prints the migration recovery command (exit 2); validates v2 fully, including a replay comparison of derived indices. It must not imply that legacy v1 is damaged. Directory/list probing retains omission semantics for unsupported files; no new implicit migration/discovery subsystem.
470
470
  - Open existing canonical path create-disabled. A read-only preflight establishes owned known schema/version before any persistent pragma or DDL. Require existing DELETE journal mode, refuse another mode without changing it; the shipped v1 creator/writer already establishes DELETE. Set connection busy_timeout=0 and synchronous=FULL. Acquire BEGIN IMMEDIATE, re-check format/schema under the lock, and validate the full v1 snapshot inside it. If v2 is already present and has valid framing/header, return already-format-2 without changing revision. A second migrator follows this same in-lock check.
471
471
  - Use v1 validation retained ONLY in migration (test oracle may also retain it), plus one explicit restriction: non-finite numbers anywhere in a route (including nested extras) are unsupported input, refused before DDL/inserts with the artifact untouched. They are not normally persisted as non-finite values by the v1 JSON.stringify writer; do not add a bespoke overflow-number codec or silently coerce to null. Copy normally persisted authoritative facts losslessly, preserving revision and nextActIndex, activity indices/gaps and optional field distinctions, raw receipt/lease owner keys, route order/duplicates, strings and finite fractional timestamps. Absent routes and [] both become zero rows; empty receipt-owner maps are behaviorless and may become zero receipt rows. Do not canonicalize receipts or add foreign keys that reject accepted v1 facts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.77",
3
+ "version": "0.3.78",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.77",
3
+ "version": "0.3.78",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.77",
3
+ "version": "0.3.78",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { Text } from '@earendil-works/pi-tui';
2
+ import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
3
3
  import { z } from 'zod/v4';
4
4
 
5
5
  import { createSquareApplication } from '../../dist/square-application.js';
@@ -9,6 +9,7 @@ import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/bo
9
9
  import { automaticSessionEnd, automaticSessionStart, operationEnv } from '../../dist/automatic-session.js';
10
10
  import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
11
11
  import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
12
+ import { formatActivityId } from '../../dist/square-core.js';
12
13
  import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
13
14
 
14
15
  class PiDeliveryDroppedError extends Error {
@@ -36,15 +37,17 @@ function framePiMessage(content) {
36
37
  return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
37
38
  }
38
39
 
39
- /** Renderer-only summary and full text; Pi never sends `details` to the model. */
40
+ /** Renderer-only card facts for Pi and Paseo; Pi never sends `details` to the model. */
40
41
  export function pendingCardDetails(pending) {
41
42
  const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
42
43
  square: path.basename(membership.squarePath, '.square'),
43
44
  actor: note.actor,
44
45
  recipient: membership.name,
45
46
  route: note.route,
47
+ act: formatActivityId(note.actIndex),
46
48
  preview: previewAttentionBody(note.body),
47
- description: renderAttentionDescription({ actor: note.actor, recipient: membership.name, route: note.route }),
49
+ description: renderAttentionDescription({ actor: note.actor, route: note.route }),
50
+ body: note.body,
48
51
  })));
49
52
  if (items.length === 0) return undefined;
50
53
  const fullText = pending.flatMap((membership) => membership.notifications.map((note) => renderAttentionFull({
@@ -73,31 +76,67 @@ function fitColumns(text, columns) {
73
76
  return `${out}…`;
74
77
  }
75
78
 
79
+ const fg = (theme, color, text) => theme.fg?.(color, text) ?? text;
80
+ const style = (theme, name) => (text) => theme[name]?.(text) ?? text;
81
+
82
+ function markdownTheme(theme) {
83
+ const color = (name) => (text) => fg(theme, name, text);
84
+ return {
85
+ heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
86
+ codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
87
+ quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
88
+ bold: style(theme, 'bold'), italic: style(theme, 'italic'), underline: style(theme, 'underline'),
89
+ strikethrough: style(theme, 'strikethrough'),
90
+ };
91
+ }
92
+
93
+ /** One styled line that never exceeds the width; truncated lines stay unstyled so escapes are never cut. */
94
+ function fitLine(parts, width) {
95
+ const plain = parts.map(([text]) => text).join('');
96
+ return widthOf(plain) <= width ? parts.map(([text, paint]) => paint(text)).join('') : fitColumns(plain, width);
97
+ }
98
+
76
99
  /**
77
- * Collapsed: one line naming who reached you and where. Expanded: the complete
78
- * notification text from renderer details, wrapped by Pi's terminal component.
100
+ * Collapsed: who reached you and where, then the first line of what they said.
101
+ * Expanded: each activity as a heading line and its Markdown body; the model's
102
+ * framing and continuation hints are not for human eyes.
79
103
  */
80
104
  export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
81
- if (expanded) {
82
- return typeof message.details?.fullText === 'string'
83
- ? new Text(message.details.fullText, outputPad, 0)
84
- : undefined;
85
- }
86
105
  const items = message.details?.items;
87
- if (!Array.isArray(items) || items.length === 0) return undefined;
88
- const [first] = items;
89
- const tail = ` · ${first.square}${items.length > 1 ? ` +${items.length - 1}` : ''}`;
90
- return {
91
- invalidate() {},
92
- render(width) {
93
- const room = Math.max(1, width - outputPad * 2 - 2);
94
- // Style only an untruncated line, so escape codes never count toward or get cut by the width.
95
- const line = widthOf(first.description + tail) <= room
96
- ? `${first.description}${theme.fg('dim', tail)}`
97
- : fitColumns(first.description + tail, room);
98
- return [`${' '.repeat(outputPad)}${theme.fg('customMessageLabel', '■')} ${line}`];
99
- },
100
- };
106
+ if (!Array.isArray(items) || items.length === 0 || items.some((item) => typeof item.body !== 'string')) return undefined;
107
+ const pad = ' '.repeat(outputPad);
108
+ const dim = (text) => fg(theme, 'dim', text);
109
+ if (!expanded) {
110
+ const [first] = items;
111
+ const more = items.length > 1 ? ` +${items.length - 1}` : '';
112
+ // A glance, not a render: drop the commonest inline markers so the line reads as speech.
113
+ const opening = (first.body.split('\n').find((line) => line.trim() !== '') ?? '')
114
+ .trim().replace(/^(#{1,6}|>|[-*+])\s+/, '').replace(/\*\*|__|`/g, '');
115
+ return {
116
+ invalidate() {},
117
+ render(width) {
118
+ const room = Math.max(1, width - outputPad * 2 - 2);
119
+ return [
120
+ `${pad}${fg(theme, 'customMessageLabel', '■')} ${fitLine([[first.description, (t) => t], [` · ${first.square} · ${first.recipient}${more}`, dim]], room)}`,
121
+ ...(opening ? [`${pad} ${fitLine([[opening, dim]], room)}`] : []),
122
+ ];
123
+ },
124
+ };
125
+ }
126
+ const card = new Container();
127
+ for (const [index, item] of items.entries()) {
128
+ if (index > 0) card.addChild(new Spacer(1));
129
+ card.addChild({
130
+ invalidate() {},
131
+ render(width) {
132
+ const room = Math.max(1, width - outputPad * 2 - 2);
133
+ return [`${pad}${fg(theme, 'customMessageLabel', '●')} ${fitLine([[item.description, (t) => t], [` ${item.square} · ${item.recipient} · ${item.act}`, dim]], room)}`];
134
+ },
135
+ });
136
+ card.addChild(new Spacer(1));
137
+ card.addChild(new Markdown(item.body, outputPad + 2, 0, markdownTheme(theme)));
138
+ }
139
+ return card;
101
140
  }
102
141
 
103
142
  export default function squarePiExtension(pi) {