@astrosheep/square 0.3.74 → 0.3.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attention-presentation.d.ts +2 -0
- package/dist/attention-presentation.js +11 -2
- package/dist/cli/express-presentation.js +1 -2
- package/dist/cli/ledger-command.js +6 -2
- package/dist/cli/meta-commands.js +1 -1
- package/dist/decisions.js +6 -21
- package/dist/help.js +4 -4
- package/dist/host-ledger-migration.d.ts +1 -0
- package/dist/host-ledger-migration.js +62 -21
- package/dist/host-ledger-sqlite.d.ts +4 -1
- package/dist/host-ledger-sqlite.js +36 -9
- package/dist/model.d.ts +2 -3
- package/dist/presentation.js +1 -1
- package/dist/square-application.js +1 -1
- package/guides/host-ledger.md +2 -2
- package/package.json +2 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +10 -5
- package/plugins/paseo/client/markdown.tsx +221 -0
- package/plugins/paseo/client/square-message.tsx +64 -94
- package/plugins/paseo/client/transform.ts +4 -1
- package/plugins/paseo/shared/message.ts +6 -4
- package/plugins/pi/square-pi.js +22 -8
- package/skills/square/SKILL.md +1 -1
|
@@ -12,4 +12,6 @@ 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
14
|
export declare function renderAttentionDescription({ actor, recipient, route }: Pick<AttentionPreview, 'actor' | 'recipient' | 'route'>): string;
|
|
15
|
+
/** Full notification text for human-facing expansion, never the bounded model preview. */
|
|
16
|
+
export declare function renderAttentionFull(attention: AttentionPreview): string;
|
|
15
17
|
export declare function renderAttentionPreview(attention: AttentionPreview): string;
|
|
@@ -24,14 +24,23 @@ export function renderAttentionDescription({ actor, recipient, route }) {
|
|
|
24
24
|
case 'bell': return `${actor} rang the bell · everyone’s attention`;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** Full notification text for human-facing expansion, never the bounded model preview. */
|
|
28
|
+
export function renderAttentionFull(attention) {
|
|
29
|
+
return renderAttentionBlock(attention, attention.body);
|
|
30
|
+
}
|
|
31
|
+
function renderAttentionBlock(attention, body) {
|
|
28
32
|
return [
|
|
29
33
|
'<square-activity>',
|
|
30
34
|
`· ${displayAttentionPath(attention.squarePath)} · ${formatActivityId(attention.actIndex)}`,
|
|
31
35
|
`● ${renderAttentionDescription(attention)}`,
|
|
32
36
|
'',
|
|
33
|
-
|
|
37
|
+
body,
|
|
34
38
|
'</square-activity>',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
export function renderAttentionPreview(attention) {
|
|
42
|
+
return [
|
|
43
|
+
renderAttentionBlock(attention, previewAttentionBody(attention.body)),
|
|
35
44
|
// A clipped body ends with the full command to read it all: a labeled caption, then the
|
|
36
45
|
// command bare on its own line so it can be copied and run as it is.
|
|
37
46
|
...(attentionBodyIsClipped(attention.body)
|
|
@@ -156,8 +156,7 @@ export async function executeExpression(application, squarePath, requestedName,
|
|
|
156
156
|
const lines = [message, `· draft kept: ${draftPath}`];
|
|
157
157
|
const recipientFailure = detail?.subject === 'recipient';
|
|
158
158
|
const repair = reason === 'bell_mention_conflict' ? { bell: true, fromBody: true }
|
|
159
|
-
: reason === '
|
|
160
|
-
: reason === 'not_standing' && recipientFailure ? { bell: false, fromBody: false } : undefined;
|
|
159
|
+
: reason === 'not_standing' && recipientFailure ? { bell: false, fromBody: false } : undefined;
|
|
161
160
|
if (isSquareError(error)) {
|
|
162
161
|
if ((reason === 'never_joined' && detail?.subject === 'caller') || error.code === 'unknown_participant' || error.code === 'not_joined') {
|
|
163
162
|
lines.push(joinRecoveryCommand(squarePath, name));
|
|
@@ -4,6 +4,7 @@ import { usage } from './context.js';
|
|
|
4
4
|
function renderReport(report) {
|
|
5
5
|
const lines = [
|
|
6
6
|
`host ledger: ${report.status}`,
|
|
7
|
+
`root: ${report.rootPath}`,
|
|
7
8
|
`presence: ${report.presence} · evidence: ${report.evidence}`,
|
|
8
9
|
];
|
|
9
10
|
if (report.presenceRevision !== undefined) {
|
|
@@ -21,9 +22,12 @@ function renderReport(report) {
|
|
|
21
22
|
}
|
|
22
23
|
if (report.status === 'legacy') {
|
|
23
24
|
if (report.ready)
|
|
24
|
-
lines.push('Stop
|
|
25
|
+
lines.push('Stop agent sessions and MCP services using Square with this ledger before conversion.', 'Keep them stopped until migration and verification succeed; then start them with the updated Square version.', 'Next: migrate, then verify.', 'square ledger migrate', 'square ledger verify');
|
|
25
26
|
else
|
|
26
|
-
lines.push('Unsafe legacy records; conversion refused.');
|
|
27
|
+
lines.push('Unsafe legacy records; conversion refused. Preserve the sources and resolve the rejected records before retrying verification.');
|
|
28
|
+
}
|
|
29
|
+
if (report.status === 'migrated' || report.status === 'upgraded') {
|
|
30
|
+
lines.push('Keep affected sessions and MCP services stopped until verification succeeds.', 'After verification, start them with the updated Square version.', 'square ledger verify');
|
|
27
31
|
}
|
|
28
32
|
return lines.join('\n') + '\n';
|
|
29
33
|
}
|
|
@@ -12,7 +12,7 @@ export const helpCommand = {
|
|
|
12
12
|
return renderGlobalHelp();
|
|
13
13
|
const rendered = renderSubcommandHelp(intent.command);
|
|
14
14
|
if (rendered === undefined)
|
|
15
|
-
fail(`unknown command: ${intent.command}\nrun 'square help' to list available commands`);
|
|
15
|
+
fail(`unknown command: ${intent.command}\nrun 'square --help' to list available commands`);
|
|
16
16
|
return rendered;
|
|
17
17
|
},
|
|
18
18
|
present: (result) => process.stdout.write(result),
|
package/dist/decisions.js
CHANGED
|
@@ -107,30 +107,15 @@ export function decideAct(facts, input) {
|
|
|
107
107
|
throw activityRefusal('invalid_args', `${participantIdentity(requested)} is not standing in this square`, { reason: 'not_standing', targets: [requested] });
|
|
108
108
|
addMention(resolved);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
unmatched.push(candidate);
|
|
116
|
-
else
|
|
117
|
-
addMention(resolved);
|
|
118
|
-
}
|
|
119
|
-
if (reach !== 'bell' && unmatched.length > 0) {
|
|
120
|
-
const names = roster.map((participant) => participant.name);
|
|
121
|
-
const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate, names) !== undefined);
|
|
122
|
-
const unknown = unmatched.filter((candidate) => !notStanding.includes(candidate));
|
|
123
|
-
const lines = [];
|
|
124
|
-
if (unknown.length > 0)
|
|
125
|
-
lines.push(`${unknown.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${unknown.length === 1 ? 'does' : 'do'} not match anyone in this square`);
|
|
126
|
-
if (notStanding.length > 0)
|
|
127
|
-
lines.push(`${notStanding.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${notStanding.length === 1 ? 'is' : 'are'} not standing in this square`);
|
|
128
|
-
throw activityRefusal('invalid_args', lines.join('\n'), { reason: 'unmatched_mention', targets: unmatched.map((candidate) => candidate.token) });
|
|
129
|
-
}
|
|
110
|
+
const bodyMentions = input.noMention === true ? [] : scanMentionCandidates(body);
|
|
111
|
+
for (const candidate of bodyMentions) {
|
|
112
|
+
const resolved = resolveNameAt(body, candidate, joinedNames);
|
|
113
|
+
if (resolved !== undefined)
|
|
114
|
+
addMention(resolved);
|
|
130
115
|
}
|
|
131
116
|
if (reach === 'bell' && mentionNames.length > 0)
|
|
132
117
|
throw activityRefusal('invalid_args', 'A bell cannot be combined with a mention', { reason: 'bell_mention_conflict', targets: mentionNames });
|
|
133
|
-
if (input.requireExplicitReach && reach !== 'bell' && mentionNames.length === 0 && reply === undefined && input.noMention !== true) {
|
|
118
|
+
if (input.requireExplicitReach && reach !== 'bell' && mentionNames.length === 0 && bodyMentions.length === 0 && reply === undefined && input.noMention !== true) {
|
|
134
119
|
throw activityRefusal('invalid_args', 'Express needs a recipient, reply, bell, or explicit bare reach', { reason: 'explicit_reach_required' });
|
|
135
120
|
}
|
|
136
121
|
const header = facts.header();
|
package/dist/help.js
CHANGED
|
@@ -77,12 +77,12 @@ const COMMANDS = [
|
|
|
77
77
|
{
|
|
78
78
|
names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
|
|
79
79
|
summary: 'Convert an existing version 1 square to row storage in place.',
|
|
80
|
-
details: ['For the selected square artifact only
|
|
80
|
+
details: ['For the selected .square artifact only. Global identity and delivery records use square ledger migrate instead.', 'Stop sessions and services using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'A version 2 square is left untouched; an unrelated or unsupported artifact is refused without a write. There is no --check, --backup or --force.'],
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
names: ['ledger'], usage: 'ledger <verify|migrate|gc>', usesSquare: false, group: 'maintenance',
|
|
84
84
|
summary: 'Verify, convert, or reclaim the machine-local host ledger.',
|
|
85
|
-
details: ['verify: read-only SQLite integrity or full legacy readiness with accepted/ignored/rejected counts and source hashes; not-ready exits 2, an empty root stays empty. An older
|
|
85
|
+
details: ['verify: read-only SQLite integrity or full legacy readiness with accepted/ignored/rejected counts and source hashes; not-ready exits 2, an empty root stays empty. An older supported SQLite format requires explicit migration. Unsupported versions or invalid databases need diagnosis; migration is not a repair command.', 'migrate: stop agent sessions and MCP services using Square with this ledger first, then convert legacy NDJSON or upgrade supported SQLite version 1 in place. Keep them stopped, run square ledger verify afterward, then start them with the updated Square version. Sources, records, order and observer revisions are preserved; an existing current ledger is an untouched no-op.', 'gc [--dry-run]: reclaim old wake/presentation evidence only where the square artifact proves the recipient is closed — the activity reached them and catch receipted it or they later rejoined. Dispatching, lifecycle and unprovable rows are retained. Deleted rows free reusable pages, not file size (no VACUUM); retired Claude recognition tokens keep growing in total.', 'Run gc with --dry-run first: it reports eligible, retired, cas-miss and unreadable-artifact counts without writing. Eligible rows are candidates only: a changed record or an unusable Claude correlation is retained.', 'No participant or square location is required. Uses SQUARE_HOST_LEDGER_ROOT, then SQUARE_REGISTRY’s directory, then ~/.square/host-ledger.', 'These commands never stop, restart, install into or schedule hosts; conversion and schema upgrade are never automatic. No --force or repair.'],
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
88
|
function definitionFor(command) {
|
|
@@ -131,7 +131,7 @@ export function renderSubcommandHelp(command) {
|
|
|
131
131
|
'Help:',
|
|
132
132
|
' -h, --help Show this command help.',
|
|
133
133
|
'',
|
|
134
|
-
"Run 'square help' to list available commands.",
|
|
134
|
+
"Run 'square --help' to list available commands.",
|
|
135
135
|
'',
|
|
136
136
|
].join('\n');
|
|
137
137
|
}
|
|
@@ -164,5 +164,5 @@ export function helpRequest(rawArgs) {
|
|
|
164
164
|
export function commandUsageHint(command) {
|
|
165
165
|
return command !== undefined && definitionFor(command) !== undefined
|
|
166
166
|
? `Run 'square ${command} --help' for usage.\n`
|
|
167
|
-
: "Run 'square help' to list commands.\n";
|
|
167
|
+
: "Run 'square --help' to list commands.\n";
|
|
168
168
|
}
|
|
@@ -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, } from './host-ledger-sqlite.js';
|
|
8
|
+
import { LEDGER_FILENAME, initializeLedger, insertLedgerRecord, ledgerRecordColumns, setLedgerRevisions, upgradeLedger, validateLedger, verifyLedger, inspectLedgerIdentity, } 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,21 +25,45 @@ async function exists(file) {
|
|
|
25
25
|
throw error;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
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
31
|
}
|
|
32
|
-
|
|
32
|
+
function verificationFailure(root, file, format) {
|
|
33
|
+
const name = file ?? path.join(root, LEDGER_FILENAME);
|
|
34
|
+
const detail = format === 'unsupported'
|
|
35
|
+
? 'It has an unsupported format version.'
|
|
36
|
+
: format === 'unrelated'
|
|
37
|
+
? 'It is unrelated to a Square host ledger.'
|
|
38
|
+
: format === undefined
|
|
39
|
+
? 'It could not be read.'
|
|
40
|
+
: 'Its identity is current but its contents failed verification.';
|
|
41
|
+
return new Error(`Host ledger verification failed at ${name}; ${detail}\nDestination left untouched. Migration is not a repair command. Keep the affected sessions and MCP services stopped, preserve the ledger, and diagnose the underlying file before retrying.`);
|
|
42
|
+
}
|
|
43
|
+
async function verifyDatabase(file, root, signal) {
|
|
33
44
|
abort(signal);
|
|
34
45
|
let db;
|
|
35
46
|
try {
|
|
36
47
|
db = new DatabaseSync(file, { readOnly: true });
|
|
37
48
|
const counts = await inSqliteTransaction(db, false, () => validateLedger(db), () => verifyLedger(db), signal);
|
|
38
|
-
return { status: 'sqlite', ...counts };
|
|
49
|
+
return { status: 'sqlite', rootPath: root, ...counts };
|
|
39
50
|
}
|
|
40
51
|
catch {
|
|
41
52
|
abort(signal);
|
|
42
|
-
|
|
53
|
+
let format;
|
|
54
|
+
try {
|
|
55
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
56
|
+
format = inspectLedgerIdentity(db);
|
|
57
|
+
db.close();
|
|
58
|
+
db = undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
db?.close();
|
|
62
|
+
db = undefined;
|
|
63
|
+
}
|
|
64
|
+
if (format === 'old-sqlite')
|
|
65
|
+
throw new Error(migrationGuidance(root, file));
|
|
66
|
+
throw verificationFailure(root, file, format);
|
|
43
67
|
}
|
|
44
68
|
finally {
|
|
45
69
|
db?.close();
|
|
@@ -94,9 +118,10 @@ async function readLegacy(root, kind, signal) {
|
|
|
94
118
|
async function readSources(root, signal) {
|
|
95
119
|
return { presence: await readLegacy(root, 'presence', signal), evidence: await readLegacy(root, 'evidence', signal) };
|
|
96
120
|
}
|
|
97
|
-
function sourceReport(sources) {
|
|
121
|
+
function sourceReport(root, sources) {
|
|
98
122
|
return {
|
|
99
123
|
status: sources.presence.report.sha256 === null && sources.evidence.report.sha256 === null ? 'empty' : 'legacy',
|
|
124
|
+
rootPath: root,
|
|
100
125
|
presence: sources.presence.rows.length, evidence: sources.evidence.rows.length,
|
|
101
126
|
sources: { presence: sources.presence.report, evidence: sources.evidence.report },
|
|
102
127
|
ready: sources.presence.report.rejected === 0 && sources.evidence.report.rejected === 0,
|
|
@@ -108,8 +133,8 @@ export async function verifyHostLedger(options = {}) {
|
|
|
108
133
|
abort(options.signal);
|
|
109
134
|
const destination = path.join(root, LEDGER_FILENAME);
|
|
110
135
|
if (await exists(destination))
|
|
111
|
-
return verifyDatabase(destination, options.signal);
|
|
112
|
-
return sourceReport(await readSources(root, options.signal));
|
|
136
|
+
return verifyDatabase(destination, root, options.signal);
|
|
137
|
+
return sourceReport(root, await readSources(root, options.signal));
|
|
113
138
|
}
|
|
114
139
|
/** The fixed offline fence: ownership claim, then both legacy source locks, in that order. */
|
|
115
140
|
function withOfflineLocks(root, signal, body) {
|
|
@@ -124,24 +149,24 @@ function withOfflineLocks(root, signal, body) {
|
|
|
124
149
|
async function currentFormat(root, signal) {
|
|
125
150
|
const destination = path.join(root, LEDGER_FILENAME);
|
|
126
151
|
try {
|
|
127
|
-
return await verifyDatabase(destination, signal);
|
|
152
|
+
return await verifyDatabase(destination, root, signal);
|
|
128
153
|
}
|
|
129
154
|
catch {
|
|
130
155
|
abort(signal);
|
|
131
156
|
}
|
|
132
157
|
if (!await exists(destination))
|
|
133
|
-
throw verificationFailure();
|
|
158
|
+
throw verificationFailure(root);
|
|
134
159
|
let db;
|
|
135
160
|
try {
|
|
136
161
|
db = new DatabaseSync(destination);
|
|
137
162
|
const upgraded = await inSqliteTransaction(db, true, () => { }, () => upgradeLedger(db), signal);
|
|
138
163
|
db.close();
|
|
139
164
|
db = undefined;
|
|
140
|
-
return { ...await verifyDatabase(destination, signal), status: upgraded ? 'upgraded' : 'sqlite' };
|
|
165
|
+
return { ...await verifyDatabase(destination, root, signal), status: upgraded ? 'upgraded' : 'sqlite' };
|
|
141
166
|
}
|
|
142
167
|
catch {
|
|
143
168
|
abort(signal);
|
|
144
|
-
throw verificationFailure();
|
|
169
|
+
throw verificationFailure(root, destination);
|
|
145
170
|
}
|
|
146
171
|
finally {
|
|
147
172
|
db?.close();
|
|
@@ -154,16 +179,32 @@ export async function migrateHostLedger(options = {}) {
|
|
|
154
179
|
const destination = path.join(root, LEDGER_FILENAME);
|
|
155
180
|
if (await exists(destination)) {
|
|
156
181
|
try {
|
|
157
|
-
return await verifyDatabase(destination, signal);
|
|
182
|
+
return await verifyDatabase(destination, root, signal);
|
|
158
183
|
}
|
|
159
184
|
catch {
|
|
160
185
|
abort(signal);
|
|
161
186
|
}
|
|
162
|
-
//
|
|
163
|
-
|
|
187
|
+
// Inspect only the database identity; record/schema verification remains the migration fence.
|
|
188
|
+
let format;
|
|
189
|
+
let probe;
|
|
190
|
+
try {
|
|
191
|
+
probe = new DatabaseSync(destination, { readOnly: true });
|
|
192
|
+
format = inspectLedgerIdentity(probe);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
format = undefined;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
probe?.close();
|
|
199
|
+
}
|
|
200
|
+
if (format === 'old-sqlite') {
|
|
201
|
+
// Only the supported prior format enters the existing offline upgrade fence.
|
|
202
|
+
return withOfflineLocks(root, signal, () => currentFormat(root, signal));
|
|
203
|
+
}
|
|
204
|
+
throw verificationFailure(root, destination, format);
|
|
164
205
|
}
|
|
165
206
|
if (!await exists(path.join(root, 'presence.ndjsonl')) && !await exists(path.join(root, 'evidence.ndjsonl'))) {
|
|
166
|
-
return { status: 'empty', presence: 0, evidence: 0 };
|
|
207
|
+
return { status: 'empty', rootPath: root, presence: 0, evidence: 0 };
|
|
167
208
|
}
|
|
168
209
|
return withOfflineLocks(root, signal, () => convertLegacy(root, destination, signal));
|
|
169
210
|
}
|
|
@@ -172,7 +213,7 @@ async function convertLegacy(root, destination, signal) {
|
|
|
172
213
|
if (await exists(destination))
|
|
173
214
|
return currentFormat(root, signal);
|
|
174
215
|
const sources = await readSources(root, signal);
|
|
175
|
-
const report = sourceReport(sources);
|
|
216
|
+
const report = sourceReport(root, sources);
|
|
176
217
|
if (!report.ready) {
|
|
177
218
|
const rejected = ['presence', 'evidence'].filter((kind) => sources[kind].report.rejected > 0)
|
|
178
219
|
.map((kind) => `${kind}: ${sources[kind].report.rejected} rejected (first line ${sources[kind].firstRejectedLine})`);
|
|
@@ -198,7 +239,7 @@ async function convertLegacy(root, destination, signal) {
|
|
|
198
239
|
}, signal);
|
|
199
240
|
db.close();
|
|
200
241
|
db = undefined;
|
|
201
|
-
const verified = await verifyDatabase(file, signal);
|
|
242
|
+
const verified = await verifyDatabase(file, root, signal);
|
|
202
243
|
if (verified.presence !== report.presence || verified.evidence !== report.evidence) {
|
|
203
244
|
throw new Error('Host ledger import counts do not match; conversion refused.');
|
|
204
245
|
}
|
|
@@ -208,7 +249,7 @@ async function convertLegacy(root, destination, signal) {
|
|
|
208
249
|
}
|
|
209
250
|
catch (error) {
|
|
210
251
|
if (error.code === 'EEXIST')
|
|
211
|
-
return await verifyDatabase(destination, signal);
|
|
252
|
+
return await verifyDatabase(destination, root, signal);
|
|
212
253
|
throw error;
|
|
213
254
|
}
|
|
214
255
|
const directory = await fs.open(root, 'r');
|
|
@@ -9,6 +9,7 @@ export interface Stored<T> {
|
|
|
9
9
|
readonly seq: number;
|
|
10
10
|
readonly record: T;
|
|
11
11
|
}
|
|
12
|
+
export type HostLedgerFormat = 'current' | 'old-sqlite' | 'unrelated' | 'unsupported';
|
|
12
13
|
/** The complete record remains authority; these are only reproducible lookup projections. */
|
|
13
14
|
export declare function ledgerRecordColumns(kind: Kind, input: object): Record<string, SQLInputValue>;
|
|
14
15
|
export declare function initializeLedger(db: DatabaseSync): void;
|
|
@@ -28,9 +29,11 @@ export declare function verifyLedger(db: DatabaseSync): {
|
|
|
28
29
|
presenceRevision: number;
|
|
29
30
|
evidenceRevision: number;
|
|
30
31
|
};
|
|
32
|
+
/** Read the file-type stamp and format version without making the runtime authority. */
|
|
33
|
+
export declare function inspectLedgerIdentity(db: DatabaseSync): HostLedgerFormat;
|
|
31
34
|
/** Explicit offline migration only; the caller owns the write transaction. Preserve all records and revisions. */
|
|
32
35
|
export declare function upgradeLedger(db: DatabaseSync): boolean;
|
|
33
|
-
export declare function migrationRequired(): Error;
|
|
36
|
+
export declare function migrationRequired(root: string): Error;
|
|
34
37
|
/** Transaction-local storage rows; sequence coordinates never cross HostLedgerPort. */
|
|
35
38
|
export declare class LedgerTransaction {
|
|
36
39
|
private readonly db;
|
|
@@ -137,6 +137,21 @@ function verifyLedgerAtVersion(db, version) {
|
|
|
137
137
|
throw invalid('records without revision');
|
|
138
138
|
return { ...counts, presenceRevision: revision.presence, evidenceRevision: revision.evidence };
|
|
139
139
|
}
|
|
140
|
+
function inspectLegacyRoot(root) {
|
|
141
|
+
return fs.existsSync(path.join(root, 'presence.ndjsonl')) || fs.existsSync(path.join(root, 'evidence.ndjsonl'));
|
|
142
|
+
}
|
|
143
|
+
/** Read the file-type stamp and format version without making the runtime authority. */
|
|
144
|
+
export function inspectLedgerIdentity(db) {
|
|
145
|
+
const application = db.prepare('PRAGMA application_id').get().application_id;
|
|
146
|
+
const version = db.prepare('PRAGMA user_version').get().user_version;
|
|
147
|
+
if (application !== LEDGER_APPLICATION_ID)
|
|
148
|
+
return 'unrelated';
|
|
149
|
+
if (version === LEDGER_VERSION)
|
|
150
|
+
return 'current';
|
|
151
|
+
if (version === 1)
|
|
152
|
+
return 'old-sqlite';
|
|
153
|
+
return 'unsupported';
|
|
154
|
+
}
|
|
140
155
|
/** Explicit offline migration only; the caller owns the write transaction. Preserve all records and revisions. */
|
|
141
156
|
export function upgradeLedger(db) {
|
|
142
157
|
if (db.prepare('PRAGMA user_version').get()?.user_version === LEDGER_VERSION) {
|
|
@@ -159,14 +174,26 @@ catch (error) {
|
|
|
159
174
|
return false;
|
|
160
175
|
throw error;
|
|
161
176
|
} }
|
|
162
|
-
export function migrationRequired() {
|
|
163
|
-
return new Error(
|
|
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.`);
|
|
164
179
|
}
|
|
165
180
|
/** Compact runtime framing. Full schema, projections and integrity belong to explicit verify. */
|
|
166
181
|
function validateLedgerHeader(db, version = LEDGER_VERSION) {
|
|
167
|
-
if (db.prepare('PRAGMA application_id').get()?.application_id !== LEDGER_APPLICATION_ID
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
if (db.prepare('PRAGMA application_id').get()?.application_id !== LEDGER_APPLICATION_ID)
|
|
183
|
+
throw invalid('unrelated identity.');
|
|
184
|
+
if (db.prepare('PRAGMA user_version').get()?.user_version !== version)
|
|
185
|
+
throw invalid('unsupported format version.');
|
|
186
|
+
}
|
|
187
|
+
/** The runtime's only upgrade door: a supported old ledger is pointed at explicit migration. */
|
|
188
|
+
function runtimeLedgerHeader(db, root) {
|
|
189
|
+
try {
|
|
190
|
+
validateLedgerHeader(db);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (inspectLedgerIdentity(db) === 'old-sqlite')
|
|
194
|
+
throw migrationRequired(root);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
170
197
|
}
|
|
171
198
|
/** Publish new empty authority only. Never initialize or replace an existing destination. */
|
|
172
199
|
function createEmptyLedger(root) {
|
|
@@ -203,8 +230,8 @@ function createEmptyLedger(root) {
|
|
|
203
230
|
async function openLedger(root, write, signal) {
|
|
204
231
|
const file = path.join(root, LEDGER_FILENAME);
|
|
205
232
|
if (!exists(file)) {
|
|
206
|
-
if (
|
|
207
|
-
throw migrationRequired();
|
|
233
|
+
if (inspectLegacyRoot(root))
|
|
234
|
+
throw migrationRequired(root);
|
|
208
235
|
if (!write)
|
|
209
236
|
return undefined;
|
|
210
237
|
if (signal?.aborted)
|
|
@@ -318,7 +345,7 @@ export async function withLedger(root, write, select, absent, signal) {
|
|
|
318
345
|
if (!db)
|
|
319
346
|
return absent;
|
|
320
347
|
try {
|
|
321
|
-
return await inSqliteTransaction(db, write, () =>
|
|
348
|
+
return await inSqliteTransaction(db, write, () => runtimeLedgerHeader(db, root), () => {
|
|
322
349
|
const rows = new LedgerTransaction(db), result = select(rows);
|
|
323
350
|
rows.commitRevisions();
|
|
324
351
|
return result;
|
|
@@ -333,7 +360,7 @@ export async function readLedgerRevisions(root, signal) {
|
|
|
333
360
|
if (!db)
|
|
334
361
|
return undefined;
|
|
335
362
|
try {
|
|
336
|
-
return await inSqliteTransaction(db, false, () =>
|
|
363
|
+
return await inSqliteTransaction(db, false, () => runtimeLedgerHeader(db, root), () => ledgerRevisions(db), signal);
|
|
337
364
|
}
|
|
338
365
|
finally {
|
|
339
366
|
db.close();
|
package/dist/model.d.ts
CHANGED
|
@@ -20,9 +20,8 @@ export type InternalSquareErrorCode = SquareErrorCode | 'not_found' | 'cap_reach
|
|
|
20
20
|
export interface SquareErrorFacts {
|
|
21
21
|
/** Express admission rejected before any activity was committed. */
|
|
22
22
|
activityUnsent?: true;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'unmatched_mention' | 'not_addressed' | 'migration_required' | 'explicit_reach_required' | 'invalid_reply' | 'invalid_done_body';
|
|
23
|
+
/** Refusal reason, including absent participants, conflicting reach, and inaccessible activities. */
|
|
24
|
+
reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'explicit_reach_required' | 'invalid_reply' | 'invalid_done_body';
|
|
26
25
|
/** The activity id a `catch --id` refusal named. */
|
|
27
26
|
activityId?: string;
|
|
28
27
|
/** Requested recipient spellings, without parsing presentation copy. */
|
package/dist/presentation.js
CHANGED
|
@@ -39,7 +39,7 @@ export function formatRefusal(squarePath, bodyLines, opts = {}) {
|
|
|
39
39
|
}
|
|
40
40
|
export function refusalText(error) {
|
|
41
41
|
const text = error instanceof Error ? error.message : String(error);
|
|
42
|
-
const marked = isSquareError(error) && ['never_joined', 'not_standing'
|
|
42
|
+
const marked = isSquareError(error) && ['never_joined', 'not_standing'].includes(error.facts?.reason ?? '');
|
|
43
43
|
return marked ? text.split('\n').map((line) => `✕ ${line}`).join('\n') : text;
|
|
44
44
|
}
|
|
45
45
|
export function quoteShell(value) {
|
|
@@ -134,7 +134,7 @@ async function landExpression(square, name, body, options, transport, control) {
|
|
|
134
134
|
}
|
|
135
135
|
catch (error) {
|
|
136
136
|
const recipient = isSquareError(error)
|
|
137
|
-
&& ['not_standing', '
|
|
137
|
+
&& ['not_standing', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
|
|
138
138
|
throw failure(error, {
|
|
139
139
|
operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect,
|
|
140
140
|
});
|
package/guides/host-ledger.md
CHANGED
|
@@ -22,7 +22,7 @@ Verification is read-only. It checks SQLite identity, version, schema, integrity
|
|
|
22
22
|
|
|
23
23
|
To upgrade an NDJSON ledger:
|
|
24
24
|
|
|
25
|
-
1. Stop **every**
|
|
25
|
+
1. Stop **every** agent session and MCP service using Square with this ledger, including interactive harnesses and background agents. Do not run old and new ones together.
|
|
26
26
|
2. Keep a backup of the stopped legacy files.
|
|
27
27
|
3. Run:
|
|
28
28
|
|
|
@@ -36,7 +36,7 @@ To upgrade an NDJSON ledger:
|
|
|
36
36
|
|
|
37
37
|
Migration does not stop or restart hosts. Its locks fence compatible writers but do not prove hosts are stopped. It acquires the ownership claim fence, then `presence.ndjsonl.lock`, then `evidence.ndjsonl.lock`.
|
|
38
38
|
|
|
39
|
-
An older SQLite ledger format is upgraded in place by the same `square ledger migrate` under the same stopped-
|
|
39
|
+
An older SQLite ledger format is upgraded in place by the same `square ledger migrate` under the same stopped-sessions prerequisite and the same lock order. The upgrade preserves every record, duplicate, original order and both observer revisions. `square ledger verify` stays read-only: it reports a supported old format with the migrate command and never upgrades silently. An unsupported format version or an unrelated database is refused with a diagnosis path instead: migration is not a repair command, and a failed migration is never answered by rerunning it blindly.
|
|
40
40
|
|
|
41
41
|
The importer preserves all ages, future records, duplicates, original order, native correlation and residual fields. It ignores malformed JSON, wrong versions and records without a numeric timestamp, as the old reader did, and reports those counts. Any valid version-1 record whose indexed fields are unsafe refuses the entire conversion rather than losing evidence. Refusal identifies only source kind, rejected count and first rejected line; no accepted subset is published. Legacy unknown wake sources are normalized once during import.
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.75",
|
|
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": {
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
]
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
+
"@earendil-works/pi-tui": "^1.3.0",
|
|
80
81
|
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
81
82
|
"cross-spawn": "^7.0.6",
|
|
82
83
|
"zod": "^4.6.5"
|
|
@@ -65,7 +65,7 @@ The ownership boundary belongs here. Does this match your read?
|
|
|
65
65
|
EOF
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
**Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off
|
|
68
|
+
**Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off. An unmatched `@name` stays ordinary body text and never blocks landing; only standing matches become recipients. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
|
|
69
69
|
|
|
70
70
|
Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`, `--bell`, or an `@name` in the body. The CLI rejects an omitted mode and rejects combining these flag modes. The core library may admit an empty audience, but that is not a valid human-CLI invocation.
|
|
71
71
|
|
package/plugins/paseo/README.md
CHANGED
|
@@ -26,13 +26,18 @@ plain-text detail are projected into a card. Real tools, other custom messages,
|
|
|
26
26
|
and unrecognized rows remain unchanged.
|
|
27
27
|
|
|
28
28
|
The collapsed heading uses `metadata.details.items`; it never reverse-parses
|
|
29
|
-
the content. Expansion shows `
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
the content. Expansion shows `metadata.details.fullText` when available: complete
|
|
30
|
+
notification bodies, even when the model preview was clipped or omitted. Older
|
|
31
|
+
messages fall back to `detail.text`, including clipping notices and catch commands.
|
|
32
|
+
Long content scrolls inside a bounded panel. The
|
|
33
|
+
card does not fetch extra activity or mark anything seen. Live events and fetched history use
|
|
32
34
|
the same transformer.
|
|
33
35
|
|
|
34
|
-
The card
|
|
35
|
-
|
|
36
|
+
The card is visually marked as Square: an accent glyph, a SQUARE badge, and the
|
|
37
|
+
square's name sit above the actor line, on a raised surface distinct from ordinary
|
|
38
|
+
tool output. It uses Paseo theme colors, selectable text, accessible expansion state,
|
|
39
|
+
and compact layout on narrow displays. Expansion starts at the top-right arrow;
|
|
40
|
+
collapse returns from the arrow below the expanded content. Removing the plugin restores Paseo's
|
|
36
41
|
ordinary custom-message row.
|
|
37
42
|
|
|
38
43
|
## Develop
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Text, View, type TextStyle } from "react-native";
|
|
3
|
+
import type { PluginTheme } from "@getpaseo/plugin";
|
|
4
|
+
|
|
5
|
+
interface InlinePart { text: string; bold?: boolean; italic?: boolean; code?: boolean }
|
|
6
|
+
|
|
7
|
+
// Minimal inline grammar: **bold**, *italic*, `code`. The first marker wins;
|
|
8
|
+
// no nesting, so action lines like *leans over* survive intact.
|
|
9
|
+
const INLINE_RE = /(\*\*[^*\n]+\*\*|\*[^*\n]+\*|`[^`\n]+`)/g;
|
|
10
|
+
|
|
11
|
+
export function parseInline(line: string): InlinePart[] {
|
|
12
|
+
const parts: InlinePart[] = [];
|
|
13
|
+
let last = 0;
|
|
14
|
+
for (const match of line.matchAll(INLINE_RE)) {
|
|
15
|
+
const index = match.index ?? 0;
|
|
16
|
+
if (index > last) parts.push({ text: line.slice(last, index) });
|
|
17
|
+
const token = match[0];
|
|
18
|
+
if (token.startsWith("**")) parts.push({ text: token.slice(2, -2), bold: true });
|
|
19
|
+
else if (token.startsWith("`")) parts.push({ text: token.slice(1, -1), code: true });
|
|
20
|
+
else parts.push({ text: token.slice(1, -1), italic: true });
|
|
21
|
+
last = index + token.length;
|
|
22
|
+
}
|
|
23
|
+
if (last < line.length) parts.push({ text: line.slice(last) });
|
|
24
|
+
return parts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Block =
|
|
28
|
+
| { kind: "code"; text: string }
|
|
29
|
+
| { kind: "heading"; level: number; text: string }
|
|
30
|
+
| { kind: "quote"; lines: string[] }
|
|
31
|
+
| { kind: "list"; items: string[] }
|
|
32
|
+
| { kind: "paragraph"; text: string };
|
|
33
|
+
|
|
34
|
+
function parseBlocks(markdown: string): Block[] {
|
|
35
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
36
|
+
const blocks: Block[] = [];
|
|
37
|
+
let paragraph: string[] = [];
|
|
38
|
+
const flushParagraph = () => {
|
|
39
|
+
if (paragraph.length > 0) blocks.push({ kind: "paragraph", text: paragraph.join(" ") });
|
|
40
|
+
paragraph = [];
|
|
41
|
+
};
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i]!;
|
|
44
|
+
if (line.trimStart().startsWith("```")) {
|
|
45
|
+
flushParagraph();
|
|
46
|
+
const body: string[] = [];
|
|
47
|
+
i += 1;
|
|
48
|
+
while (i < lines.length && !lines[i]!.trimStart().startsWith("```")) { body.push(lines[i]!); i += 1; }
|
|
49
|
+
blocks.push({ kind: "code", text: body.join("\n") });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
53
|
+
if (heading) {
|
|
54
|
+
flushParagraph();
|
|
55
|
+
blocks.push({ kind: "heading", level: heading[1]!.length, text: heading[2]! });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (line.startsWith(">")) {
|
|
59
|
+
flushParagraph();
|
|
60
|
+
const quote: string[] = [];
|
|
61
|
+
while (i < lines.length && lines[i]!.startsWith(">")) { quote.push(lines[i]!.replace(/^>\s?/, "")); i += 1; }
|
|
62
|
+
i -= 1;
|
|
63
|
+
blocks.push({ kind: "quote", lines: quote });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (/^\s*(?:[-*+]|\d+\.)\s+/.test(line)) {
|
|
67
|
+
flushParagraph();
|
|
68
|
+
const items: string[] = [];
|
|
69
|
+
while (i < lines.length && /^\s*(?:[-*+]|\d+\.)\s+/.test(lines[i]!)) {
|
|
70
|
+
const item = /^\s*(?:[-*+]|\d+\.)\s+(.*)$/.exec(lines[i]!);
|
|
71
|
+
items.push(item?.[1] ?? lines[i]!);
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
i -= 1;
|
|
75
|
+
blocks.push({ kind: "list", items });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (line.trim() === "") { flushParagraph(); continue; }
|
|
79
|
+
paragraph.push(line.trim());
|
|
80
|
+
}
|
|
81
|
+
flushParagraph();
|
|
82
|
+
return blocks;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface SquareBlock { meta?: string; actor?: string; body: string }
|
|
86
|
+
|
|
87
|
+
// A square notification is one or more <square-activity> blocks. Split them out so the
|
|
88
|
+
// envelope lines become styled chrome and only the body goes through markdown.
|
|
89
|
+
function parseSquareBlocks(text: string): SquareBlock[] {
|
|
90
|
+
const blocks: SquareBlock[] = [];
|
|
91
|
+
const re = /<square-activity>\n([\s\S]*?)\n<\/square-activity>/g;
|
|
92
|
+
let last = 0;
|
|
93
|
+
for (const match of text.matchAll(re)) {
|
|
94
|
+
const index = match.index ?? 0;
|
|
95
|
+
const before = text.slice(last, index).trim();
|
|
96
|
+
if (before !== "") blocks.push({ body: before });
|
|
97
|
+
const inner = match[1]!;
|
|
98
|
+
const innerLines = inner.split("\n");
|
|
99
|
+
const meta = innerLines[0]?.startsWith("· ") ? innerLines.shift() : undefined;
|
|
100
|
+
const actor = innerLines[0]?.startsWith("● ") ? innerLines.shift() : undefined;
|
|
101
|
+
blocks.push({ meta, actor, body: innerLines.join("\n").trim() });
|
|
102
|
+
last = index + match[0].length;
|
|
103
|
+
}
|
|
104
|
+
const rest = text.slice(last).trim();
|
|
105
|
+
if (rest !== "") blocks.push({ body: rest });
|
|
106
|
+
if (blocks.length === 0) blocks.push({ body: text });
|
|
107
|
+
return blocks;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function SquareMarkdown({ text, theme }: { text: string; theme: PluginTheme }) {
|
|
111
|
+
const { colors } = theme;
|
|
112
|
+
const styles = useMemo(() => ({
|
|
113
|
+
block: { gap: 6 },
|
|
114
|
+
paragraph: { color: colors.foreground, fontSize: 13, lineHeight: 20 },
|
|
115
|
+
bold: { fontWeight: "700" as const },
|
|
116
|
+
italic: { fontStyle: "italic" as const },
|
|
117
|
+
inlineCode: {
|
|
118
|
+
fontFamily: "Menlo",
|
|
119
|
+
fontSize: 12,
|
|
120
|
+
backgroundColor: colors.surface2,
|
|
121
|
+
color: colors.foreground,
|
|
122
|
+
},
|
|
123
|
+
codeBlock: {
|
|
124
|
+
backgroundColor: colors.surface0,
|
|
125
|
+
borderRadius: 8,
|
|
126
|
+
borderWidth: 1,
|
|
127
|
+
borderColor: colors.border,
|
|
128
|
+
paddingHorizontal: 9,
|
|
129
|
+
paddingVertical: 7,
|
|
130
|
+
},
|
|
131
|
+
codeText: { fontFamily: "Menlo", fontSize: 11.5, lineHeight: 17, color: colors.foreground },
|
|
132
|
+
heading: { color: colors.foreground, fontWeight: "700" as const },
|
|
133
|
+
quote: {
|
|
134
|
+
borderLeftWidth: 2,
|
|
135
|
+
borderLeftColor: colors.border,
|
|
136
|
+
paddingLeft: 8,
|
|
137
|
+
gap: 2,
|
|
138
|
+
},
|
|
139
|
+
quoteText: { color: colors.foregroundMuted, fontSize: 13, lineHeight: 20, fontStyle: "italic" as const },
|
|
140
|
+
meta: { color: colors.foregroundMuted, fontSize: 11, lineHeight: 15, fontFamily: "Menlo" },
|
|
141
|
+
actor: { color: colors.accent, fontSize: 12, lineHeight: 17 },
|
|
142
|
+
separator: { height: 1, backgroundColor: colors.border, marginVertical: 4 },
|
|
143
|
+
listRow: { flexDirection: "row" as const, gap: 6 },
|
|
144
|
+
bullet: { color: colors.accent, fontSize: 13, lineHeight: 20 },
|
|
145
|
+
}), [colors]);
|
|
146
|
+
|
|
147
|
+
const renderInline = (line: string, keyPrefix: string, extra?: TextStyle) => (
|
|
148
|
+
<Text key={keyPrefix} style={[styles.paragraph, extra]}>
|
|
149
|
+
{parseInline(line).map((part, index) => (
|
|
150
|
+
<Text
|
|
151
|
+
key={`${keyPrefix}-${index}`}
|
|
152
|
+
style={[
|
|
153
|
+
part.bold ? styles.bold : undefined,
|
|
154
|
+
part.italic ? styles.italic : undefined,
|
|
155
|
+
part.code ? styles.inlineCode : undefined,
|
|
156
|
+
]}
|
|
157
|
+
>{part.code ? ` ${part.text} ` : part.text}</Text>
|
|
158
|
+
))}
|
|
159
|
+
</Text>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const renderBlock = (block: Block, key: string) => {
|
|
163
|
+
switch (block.kind) {
|
|
164
|
+
case "code":
|
|
165
|
+
return (
|
|
166
|
+
<View key={key} style={styles.codeBlock}>
|
|
167
|
+
<Text selectable style={styles.codeText}>{block.text}</Text>
|
|
168
|
+
</View>
|
|
169
|
+
);
|
|
170
|
+
case "heading":
|
|
171
|
+
return renderInline(block.text, key, { fontSize: Math.max(13, 18 - block.level), fontWeight: "700" });
|
|
172
|
+
case "quote":
|
|
173
|
+
return (
|
|
174
|
+
<View key={key} style={styles.quote}>
|
|
175
|
+
{block.lines.map((line, index) => (
|
|
176
|
+
<Text key={`${key}-${index}`} style={styles.quoteText}>{line}</Text>
|
|
177
|
+
))}
|
|
178
|
+
</View>
|
|
179
|
+
);
|
|
180
|
+
case "list":
|
|
181
|
+
return (
|
|
182
|
+
<View key={key} style={{ gap: 2 }}>
|
|
183
|
+
{block.items.map((item, index) => (
|
|
184
|
+
<View key={`${key}-${index}`} style={styles.listRow}>
|
|
185
|
+
<Text style={styles.bullet}>•</Text>
|
|
186
|
+
<Text style={[styles.paragraph, { flexShrink: 1 }]}>
|
|
187
|
+
{parseInline(item).map((part, partIndex) => (
|
|
188
|
+
<Text
|
|
189
|
+
key={partIndex}
|
|
190
|
+
style={[
|
|
191
|
+
part.bold ? styles.bold : undefined,
|
|
192
|
+
part.italic ? styles.italic : undefined,
|
|
193
|
+
part.code ? styles.inlineCode : undefined,
|
|
194
|
+
]}
|
|
195
|
+
>{part.code ? ` ${part.text} ` : part.text}</Text>
|
|
196
|
+
))}
|
|
197
|
+
</Text>
|
|
198
|
+
</View>
|
|
199
|
+
))}
|
|
200
|
+
</View>
|
|
201
|
+
);
|
|
202
|
+
case "paragraph":
|
|
203
|
+
return renderInline(block.text, key);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const blocks = useMemo(() => parseSquareBlocks(text), [text]);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<View style={styles.block}>
|
|
211
|
+
{blocks.map((block, blockIndex) => (
|
|
212
|
+
<View key={blockIndex} style={styles.block}>
|
|
213
|
+
{blockIndex > 0 && <View style={styles.separator} />}
|
|
214
|
+
{block.meta !== undefined && <Text style={styles.meta}>{block.meta}</Text>}
|
|
215
|
+
{block.actor !== undefined && <Text style={styles.actor}>{block.actor}</Text>}
|
|
216
|
+
{parseBlocks(block.body).map((inner, innerIndex) => renderBlock(inner, `${blockIndex}-${innerIndex}`))}
|
|
217
|
+
</View>
|
|
218
|
+
))}
|
|
219
|
+
</View>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -1,117 +1,87 @@
|
|
|
1
1
|
import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
|
|
2
|
-
import {
|
|
2
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
3
3
|
import { useMemo, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { Pressable, ScrollView, Text, View } from "react-native";
|
|
5
5
|
import { squareHeadline, type SquareMessageData } from "../shared/message";
|
|
6
|
+
import { parseInline, SquareMarkdown } from "./markdown";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const hex = /^#([\da-f]{3}|[\da-f]{6})$/iu.exec(color);
|
|
9
|
-
if (hex === null) return color;
|
|
10
|
-
const value = hex[1]!.length === 3 ? [...hex[1]!].map((part) => part + part).join("") : hex[1]!;
|
|
11
|
-
const alpha = Math.round(opacity * 255).toString(16).padStart(2, "0");
|
|
12
|
-
return `#${value}${alpha}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
8
|
+
/** A plain card: who, what they did, where, then what they said. No badge, no chrome — the words carry the place. */
|
|
15
9
|
export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
|
|
16
10
|
const [expanded, setExpanded] = useState(false);
|
|
17
|
-
const
|
|
18
|
-
const { text, items } = item.data;
|
|
11
|
+
const { text, fullText, items } = item.data;
|
|
19
12
|
const latest = items?.[items.length - 1];
|
|
13
|
+
// Preview typography only; expansion prefers the complete renderer-only text.
|
|
20
14
|
const preview = latest?.preview?.replace(/\s+/g, " ").trim();
|
|
21
15
|
const muted = theme.colors.foregroundMuted;
|
|
22
16
|
const foreground = theme.colors.foreground;
|
|
17
|
+
|
|
23
18
|
const styles = useMemo(() => ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
preview: { color: foreground, fontSize: 12, lineHeight: 18, flexShrink: 1 },
|
|
28
|
-
bodyPanel: {
|
|
19
|
+
card: {
|
|
20
|
+
marginVertical: 3,
|
|
21
|
+
marginRight: layout.compact ? 0 : 8,
|
|
29
22
|
backgroundColor: theme.colors.surface1,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
borderWidth: 1,
|
|
24
|
+
borderColor: theme.colors.border,
|
|
25
|
+
borderRadius: 6,
|
|
26
|
+
paddingHorizontal: 12,
|
|
27
|
+
paddingVertical: 8,
|
|
28
|
+
gap: 4,
|
|
34
29
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
|
|
30
|
+
whoRow: { flexDirection: "row" as const, alignItems: "baseline" as const, gap: 6 },
|
|
31
|
+
glyph: { color: theme.colors.accent, fontSize: 9 },
|
|
32
|
+
actor: { color: foreground, fontSize: 12.5, fontWeight: "600" as const },
|
|
33
|
+
route: { color: muted, fontSize: 12.5, flexShrink: 1 },
|
|
34
|
+
count: { color: muted, fontSize: 11 },
|
|
35
|
+
preview: { color: foreground, fontSize: 13, lineHeight: 19 },
|
|
36
|
+
action: { color: muted, fontStyle: "italic" as const },
|
|
37
|
+
bold: { fontWeight: "700" as const },
|
|
38
|
+
code: { fontFamily: "Menlo", fontSize: 12 },
|
|
39
|
+
}), [foreground, layout.compact, muted, theme.colors.accent, theme.colors.border, theme.colors.surface1]);
|
|
40
|
+
|
|
41
|
+
const headline = latest !== undefined ? squareHeadline(latest) : "Square context";
|
|
42
|
+
const routeText = latest?.actor !== undefined && headline.startsWith(latest.actor)
|
|
43
|
+
? headline.slice(latest.actor.length).trim()
|
|
44
|
+
: headline;
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
|
-
<View>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
<View style={styles.card}>
|
|
48
|
+
<Pressable
|
|
49
|
+
accessibilityRole="button"
|
|
50
|
+
accessibilityLabel={`${expanded ? "收起" : "展开"} ${latest?.square ?? "square"}: ${headline}`}
|
|
51
|
+
aria-expanded={expanded}
|
|
52
|
+
onPress={() => setExpanded(value => !value)}
|
|
53
|
+
style={({ pressed }) => ({ gap: 4, opacity: pressed ? 0.7 : 1 })}
|
|
54
|
+
>
|
|
55
|
+
<View style={styles.whoRow}>
|
|
56
|
+
<Text style={styles.glyph}>■</Text>
|
|
57
|
+
{latest?.actor !== undefined && <Text style={styles.actor}>{latest.actor}</Text>}
|
|
58
|
+
<Text numberOfLines={1} style={styles.route}>
|
|
59
|
+
{routeText}{latest?.square !== undefined && ` · ${latest.square}`}
|
|
52
60
|
</Text>
|
|
53
|
-
{items && items.length > 1 && <Text style={styles.meta}>· {items.length} landed</Text>}
|
|
54
61
|
<View style={{ flex: 1 }} />
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
accessibilityLabel={`展开 ${latest !== undefined ? squareHeadline(latest) : "Square context"}`}
|
|
58
|
-
aria-expanded={false}
|
|
59
|
-
onPress={() => setExpanded(true)}
|
|
60
|
-
hitSlop={6}
|
|
61
|
-
style={({ pressed }) => ({ padding: 5, opacity: pressed ? 0.6 : 1 })}
|
|
62
|
-
>
|
|
63
|
-
<Icon name="ChevronDown" size={18} color={muted} />
|
|
64
|
-
</Pressable>}
|
|
62
|
+
{items !== undefined && items.length > 1 && <Text style={styles.count}>{items.length}</Text>}
|
|
63
|
+
<Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={16} color={muted} />
|
|
65
64
|
</View>
|
|
66
|
-
{
|
|
67
|
-
<Text numberOfLines={
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
{!expanded && preview !== undefined && preview !== "" && (
|
|
66
|
+
<Text numberOfLines={2} style={styles.preview}>
|
|
67
|
+
{parseInline(preview).map((part, index) => (
|
|
68
|
+
<Text
|
|
69
|
+
key={index}
|
|
70
|
+
style={[
|
|
71
|
+
part.italic ? styles.action : undefined,
|
|
72
|
+
part.bold ? styles.bold : undefined,
|
|
73
|
+
part.code ? styles.code : undefined,
|
|
74
|
+
]}
|
|
75
|
+
>{part.text}</Text>
|
|
76
|
+
))}
|
|
71
77
|
</Text>
|
|
72
|
-
|
|
73
|
-
</
|
|
74
|
-
{expanded &&
|
|
75
|
-
<ScrollView style={{ maxHeight: layout.compact ? 280 : 400 }} nestedScrollEnabled>
|
|
76
|
-
<
|
|
78
|
+
)}
|
|
79
|
+
</Pressable>
|
|
80
|
+
{expanded && (
|
|
81
|
+
<ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 2 }} nestedScrollEnabled>
|
|
82
|
+
<SquareMarkdown text={fullText ?? text} theme={theme} />
|
|
77
83
|
</ScrollView>
|
|
78
|
-
|
|
79
|
-
marginHorizontal: layout.compact ? 10 : 12,
|
|
80
|
-
marginTop: 5,
|
|
81
|
-
borderTopWidth: 1,
|
|
82
|
-
borderTopColor: withOpacity(theme.colors.border, 0.55),
|
|
83
|
-
paddingVertical: 3,
|
|
84
|
-
flexDirection: "row",
|
|
85
|
-
alignItems: "center",
|
|
86
|
-
justifyContent: "space-between",
|
|
87
|
-
}}>
|
|
88
|
-
<Pressable
|
|
89
|
-
accessibilityRole="button"
|
|
90
|
-
accessibilityLabel="Copy Square content"
|
|
91
|
-
onPress={() => { void copyText(text).catch(() => toast.error("Could not copy Square content")); }}
|
|
92
|
-
style={({ pressed }) => ({
|
|
93
|
-
flexDirection: "row",
|
|
94
|
-
alignItems: "center",
|
|
95
|
-
gap: 5,
|
|
96
|
-
paddingHorizontal: 2,
|
|
97
|
-
opacity: pressed ? 0.6 : 1,
|
|
98
|
-
})}
|
|
99
|
-
>
|
|
100
|
-
<Icon name="Copy" size={12} color={muted} />
|
|
101
|
-
<Text style={{ color: muted, fontSize: 11 }}>Copy content</Text>
|
|
102
|
-
</Pressable>
|
|
103
|
-
<Pressable
|
|
104
|
-
accessibilityRole="button"
|
|
105
|
-
accessibilityLabel="收起 Square context"
|
|
106
|
-
aria-expanded={true}
|
|
107
|
-
onPress={() => setExpanded(false)}
|
|
108
|
-
hitSlop={6}
|
|
109
|
-
style={({ pressed }) => ({ paddingHorizontal: 5, paddingVertical: 1, opacity: pressed ? 0.6 : 1 })}
|
|
110
|
-
>
|
|
111
|
-
<Icon name="ChevronUp" size={18} color={muted} />
|
|
112
|
-
</Pressable>
|
|
113
|
-
</View>
|
|
114
|
-
</View>}
|
|
84
|
+
)}
|
|
115
85
|
</View>
|
|
116
86
|
);
|
|
117
87
|
}
|
|
@@ -17,7 +17,10 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
|
|
|
17
17
|
version: 1,
|
|
18
18
|
data: {
|
|
19
19
|
text: item.detail.text,
|
|
20
|
-
...(details.success ? {
|
|
20
|
+
...(details.success ? {
|
|
21
|
+
items: details.data.items,
|
|
22
|
+
...(details.data.fullText === undefined ? {} : { fullText: details.data.fullText }),
|
|
23
|
+
} : {}),
|
|
21
24
|
},
|
|
22
25
|
}],
|
|
23
26
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export const squareCardDetailsSchema = z.object({
|
|
4
|
+
fullText: z.string().optional(),
|
|
4
5
|
items: z.array(z.object({
|
|
5
6
|
square: z.string(),
|
|
6
7
|
actor: z.string().optional(),
|
|
@@ -16,15 +17,16 @@ export function squareHeadline(item: z.output<typeof squareCardDetailsSchema>["i
|
|
|
16
17
|
if (item.actor === undefined || item.route === undefined) return item.description;
|
|
17
18
|
const to = item.recipient;
|
|
18
19
|
switch (item.route) {
|
|
19
|
-
case "mention": return to === undefined ? `${item.actor}
|
|
20
|
-
case "reply": return to === undefined ? `${item.actor}
|
|
21
|
-
case "attention": return `
|
|
22
|
-
case "bell": return `${item.actor}
|
|
20
|
+
case "mention": return to === undefined ? `${item.actor} spoke your name` : `${item.actor} spoke ${to}'s name`;
|
|
21
|
+
case "reply": return to === undefined ? `${item.actor} answered you` : `${item.actor} answered ${to}`;
|
|
22
|
+
case "attention": return to === undefined ? `${item.actor} spoke · you're listening` : `${item.actor} spoke · ${to} is listening`;
|
|
23
|
+
case "bell": return `${item.actor} rang the bell`;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export const squareMessageSchema = z.object({
|
|
27
28
|
text: z.string(),
|
|
29
|
+
fullText: z.string().optional(),
|
|
28
30
|
items: squareCardDetailsSchema.shape.items.optional(),
|
|
29
31
|
});
|
|
30
32
|
export type SquareMessageData = z.output<typeof squareMessageSchema>;
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { Text } from '@earendil-works/pi-tui';
|
|
2
3
|
|
|
3
4
|
import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
|
|
4
5
|
import { automaticSessionEnd, automaticSessionStart } from '../../dist/automatic-session.js';
|
|
5
6
|
import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
|
|
6
|
-
import { previewAttentionBody, renderAttentionDescription } from '../../dist/attention-presentation.js';
|
|
7
|
+
import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
|
|
7
8
|
import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
|
|
8
9
|
|
|
9
10
|
class PiDeliveryDroppedError extends Error {
|
|
@@ -31,7 +32,7 @@ function framePiMessage(content) {
|
|
|
31
32
|
return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
/** Renderer-only
|
|
35
|
+
/** Renderer-only summary and full text; Pi never sends `details` to the model. */
|
|
35
36
|
export function pendingCardDetails(pending) {
|
|
36
37
|
const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
|
|
37
38
|
square: path.basename(membership.squarePath, '.square'),
|
|
@@ -41,7 +42,14 @@ export function pendingCardDetails(pending) {
|
|
|
41
42
|
preview: previewAttentionBody(note.body),
|
|
42
43
|
description: renderAttentionDescription({ actor: note.actor, recipient: membership.name, route: note.route }),
|
|
43
44
|
})));
|
|
44
|
-
|
|
45
|
+
if (items.length === 0) return undefined;
|
|
46
|
+
const fullText = pending.flatMap((membership) => membership.notifications.map((note) => renderAttentionFull({
|
|
47
|
+
squarePath: membership.squarePath,
|
|
48
|
+
actIndex: note.actIndex,
|
|
49
|
+
recipient: membership.name,
|
|
50
|
+
...note,
|
|
51
|
+
}))).join('\n\n');
|
|
52
|
+
return { items, fullText };
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
// Conservative terminal width: anything outside ASCII counts as two columns, so a
|
|
@@ -62,12 +70,17 @@ function fitColumns(text, columns) {
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
/**
|
|
65
|
-
* Collapsed: one line naming who reached you and where. Expanded:
|
|
66
|
-
*
|
|
73
|
+
* Collapsed: one line naming who reached you and where. Expanded: the complete
|
|
74
|
+
* notification text from renderer details, wrapped by Pi's terminal component.
|
|
67
75
|
*/
|
|
68
76
|
export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
|
|
77
|
+
if (expanded) {
|
|
78
|
+
return typeof message.details?.fullText === 'string'
|
|
79
|
+
? new Text(message.details.fullText, outputPad, 0)
|
|
80
|
+
: undefined;
|
|
81
|
+
}
|
|
69
82
|
const items = message.details?.items;
|
|
70
|
-
if (
|
|
83
|
+
if (!Array.isArray(items) || items.length === 0) return undefined;
|
|
71
84
|
const [first] = items;
|
|
72
85
|
const tail = ` · ${first.square}${items.length > 1 ? ` +${items.length - 1}` : ''}`;
|
|
73
86
|
return {
|
|
@@ -240,8 +253,8 @@ export default function squarePiExtension(pi) {
|
|
|
240
253
|
boundSessionId,
|
|
241
254
|
async (content) => {
|
|
242
255
|
if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
|
|
243
|
-
// One card for the human:
|
|
244
|
-
//
|
|
256
|
+
// One card for the human: a compact heading, expandable to the full body.
|
|
257
|
+
// Model context stays bounded; renderer-only details carry the complete text.
|
|
245
258
|
const messageContent = framePiMessage(content);
|
|
246
259
|
const landing = waitForLanding(messageContent, signal, 'steer');
|
|
247
260
|
try {
|
|
@@ -259,6 +272,7 @@ export default function squarePiExtension(pi) {
|
|
|
259
272
|
const inbox = await observer.read({ signal: boundarySignal, excludeKeys: handledPending });
|
|
260
273
|
if (signal.aborted || token !== generation) return [];
|
|
261
274
|
for (const key of inboxKeys(inbox)) observedPending.add(key);
|
|
275
|
+
pending = inbox;
|
|
262
276
|
return inbox;
|
|
263
277
|
},
|
|
264
278
|
env,
|
package/skills/square/SKILL.md
CHANGED
|
@@ -65,7 +65,7 @@ The ownership boundary belongs here. Does this match your read?
|
|
|
65
65
|
EOF
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
**Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off
|
|
68
|
+
**Addressing.** Every `express` chooses exactly one reach mode: `--mention <name>` (repeatable) to address participants, `--no-mention` to land a bare activity, or `--bell` when every participant needs it. An `@name` in the body that matches a standing participant also addresses them — code spans, emails, and URLs never count, and backticks keep literal `@text` literal; `--no-mention` switches that scan off. An unmatched `@name` stays ordinary body text and never blocks landing; only standing matches become recipients. Mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. `listen` opts a participant into future bare delivery. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
|
|
69
69
|
|
|
70
70
|
Every human-CLI `express` must choose exactly one reach mode: one or more `--mention <name>` flags, `--no-mention`, `--bell`, or an `@name` in the body. The CLI rejects an omitted mode and rejects combining these flag modes. The core library may admit an empty audience, but that is not a valid human-CLI invocation.
|
|
71
71
|
|