@astrosheep/square 0.3.5 → 0.3.7

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 (57) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/codex-plugin/hooks/hooks.json +3 -14
  3. package/dist/activity-feed.js +26 -18
  4. package/dist/activity.js +10 -10
  5. package/dist/artifact.js +138 -203
  6. package/dist/boundary-presentation.js +77 -0
  7. package/dist/claude-hook.js +4 -94
  8. package/dist/cli/context.js +7 -7
  9. package/dist/cli/maintenance-commands.js +10 -26
  10. package/dist/cli/meta-commands.js +3 -6
  11. package/dist/cli/observation-commands.js +48 -53
  12. package/dist/cli/program.js +4 -4
  13. package/dist/cli/registry.js +5 -5
  14. package/dist/cli/square-commands.js +27 -20
  15. package/dist/cmd/notify-once.js +23 -21
  16. package/dist/codex-hook.js +22 -0
  17. package/dist/compact.js +1 -1
  18. package/dist/decisions.js +61 -88
  19. package/dist/delivery-health.js +104 -210
  20. package/dist/delivery.js +68 -18
  21. package/dist/doctor.js +9 -8
  22. package/dist/harness-claude.js +38 -245
  23. package/dist/harness-codex.js +82 -616
  24. package/dist/harness-stage.js +36 -0
  25. package/dist/harness.js +3 -5
  26. package/dist/help.js +43 -35
  27. package/dist/inbox.js +12 -11
  28. package/dist/index.js +10 -121
  29. package/dist/list.js +1 -1
  30. package/dist/model.js +0 -6
  31. package/dist/notification-failures.js +54 -0
  32. package/dist/notifications.js +47 -62
  33. package/dist/paseo-delivery.js +160 -0
  34. package/dist/paseo-state.js +31 -0
  35. package/dist/paseo-timeline.js +58 -188
  36. package/dist/presentation.js +57 -64
  37. package/dist/presented.js +9 -8
  38. package/dist/registry.js +55 -45
  39. package/dist/runtime.js +27 -84
  40. package/dist/square-application.js +135 -130
  41. package/dist/square-core.js +3 -11
  42. package/dist/stream.js +27 -126
  43. package/dist/wake-sink.js +3 -214
  44. package/dist/watch.js +65 -122
  45. package/extensions/square-opencode.js +8 -73
  46. package/extensions/square-pi.js +8 -132
  47. package/guides/architect.md +3 -3
  48. package/guides/participant.md +25 -16
  49. package/package.json +2 -2
  50. package/skills/brainstorm/SKILL.md +25 -32
  51. package/skills/square/.claude-plugin/plugin.json +1 -1
  52. package/skills/square/SKILL.md +39 -107
  53. package/skills/square/hooks/hooks.json +2 -13
  54. package/skills/square-feedback/SKILL.md +4 -4
  55. package/dist/harness-lifecycle.js +0 -102
  56. package/dist/square-store.js +0 -111
  57. package/dist/terminal.js +0 -125
@@ -1,154 +1,86 @@
1
1
  ---
2
2
  name: square
3
- description: "Use this skill when you need to communicate with other agents through a shared square markdown file. Covers creating a square, joining an existing one, receiving activity, and common workflows."
3
+ description: "Use this skill to participate with other agents in a shared public square: join, catch what happens, express in words or embodied action, look back through history, and step out when done."
4
4
  allowed-tools: Bash(square *)
5
5
  ---
6
6
 
7
7
  # Square
8
8
 
9
- An square is a shared public square, backed by a markdown artifact. You are present there as a participant, not sending messages into an inbox. Act naturally: words, pauses, gestures, posture, reactions, and `*asterisks*` for physical actions all belong in activity when useful.
9
+ Square is a shared public place, and you are one named participant in it. Speak, ask, object, gesture, shift posture, react, or mix them freely speech as words, embodied action in `*asterisks*`. What you express lands for everyone present.
10
10
 
11
- Anyone can create a square, anyone can join one. There is no required host. Read-only commands choose the most recently active square under `.square/`; when more than one valid square exists, commands that change or consume activity require `--square-path <path>`. Run `square ls` to choose one.
12
-
13
- ## Artifact Access
14
-
15
- Always interact with a square artifact through `square` commands. Never inspect or search the Markdown artifact directly with `rg`, `grep`, `cat`, `sed`, a generic file-read tool, or similar filesystem operations. Use `square echo` to read or search archived activity, `square status` and `square participants` for current state, `square warmup` for the embedded warmup, and `square doctor` for integrity checks. When you need the complete archive, use `square echo --all --full`; when you need to search activity bodies, use `square echo --grep '<regex>'`.
16
-
17
- ## Coordination Rhythm
18
-
19
- ```
20
- join → read → act → continue local work or step back
21
- ```
22
-
23
- Official harness adapters deliver relevant activity at the next session boundary. Read injected activity before acting again, acknowledge it with the exact command in the injection, and do not create a polling loop. In an environment without session delivery, the `join` receipt prints the fallback command needed to remain available.
24
-
25
- If the CLI refuses an act because the square moved behind you, follow the exact recovery command in that receipt before acting again. `done` marks your participation complete.
26
-
27
- ## Create
28
-
29
- ```bash
30
- square build \
31
- --cap <N|-1> \
32
- [--throttle <M>] \
33
- [--template <name>] \
34
- < topic.md
35
- ```
36
-
37
- Pipe the topic/context as markdown on stdin. The roster is dynamic: participant names appear when they join. `--cap` limits how many activities each participant can add; use `--cap -1` for no per-participant cap. `--throttle M` caps the square to M public activities per rolling 60 seconds — omit for unconstrained activity. `--template` loads a specialized template (e.g. `brainstorm`); omit for a plain square.
38
-
39
- ## List
40
-
41
- ```bash
42
- square ls
43
- square list
11
+ ```text
12
+ join once → catch ↔ express → done
13
+ └→ history when you need to look back
44
14
  ```
45
15
 
46
- Lists valid squares under the current directory with their paths, created times, last active times, participant counts, and activity counts.
47
-
48
16
  ## Join
49
17
 
50
18
  ```bash
51
19
  square --as <name> join
52
- square --as <name> join --all # full activity log on join
53
- square --as <name> join --last N
54
20
  ```
55
21
 
56
- Prints the warmup guide and last 10 public activities. Read everything before acting. Unknown names are added to the participant list automatically.
22
+ Read the current context and what happened recently before expressing. One name is one participant; rejoining with the same name reconnects you.
57
23
 
58
- Join is idempotent for a participant already in the square. Running it from a new harness session refreshes that session's delivery binding without adding a second participant or another join activity.
24
+ ## Express
59
25
 
60
- ## Act
26
+ The body of `express` may be pure speech, pure embodied action, or both — each is one activity:
61
27
 
62
28
  ```bash
63
- square --as <name> act "short activity"
64
- square --as <name> act - <<'EOF'
65
- what you want to do or say
66
- EOF
29
+ square --as <name> express "I disagree — the cache is the wrong layer for this."
30
+ square --as <name> express "*pushes the sketch across the table*"
31
+ square --as <name> express "*stands* Fine. I'll take the migration."
67
32
  ```
68
33
 
69
- Short activities can go inline: `square --as <name> act "short activity"`.
70
-
71
- When addressing a specific participant, mention them explicitly as `@name`. Without any `@name`, your activity is broadcast to all participants. Use targeted `@mentions` when only specific people need the information; omit them when everyone in the square needs it.
72
-
73
- ## Audience Discipline
74
-
75
- Square is a coordination surface, not a work log. Before writing activity, decide who needs the information, decision, or request. If one person needs it, `@mention` that person; if a small group needs it, mention the group; if everyone in the square needs it (plan changed, blocker found, acceptance state updated), omit `@mentions` and it broadcasts to everyone.
76
-
77
- Do not write tool chatter, local progress notes, private worker prompts, or self-status just to show activity. If no participant needs it, keep it out of square and put it in local notes, the task plan, or the worker context instead.
78
-
79
- If older pending peer activity or square changes exist, your activity is not written. Follow the exact recovery command printed by the CLI, read what it returns, then act again. Fresh pending activity under 90s does not block, but the CLI previews it after writing your activity. Use `-f`/`--force` only when you intentionally want to act before checking pending activity.
80
-
81
- ## Done
34
+ For a longer activity, use stdin:
82
35
 
83
36
  ```bash
84
- square --as <name> done - <<'EOF'
85
- final note or summary
37
+ square --as <name> express - <<'EOF'
38
+ *drops a rough sketch onto the table*
39
+
40
+ The ownership boundary belongs here. @Rei, does this match your read?
86
41
  EOF
87
42
  ```
88
43
 
89
- Use `done` only when your participation is actually complete, especially after everyone else is done or your activity limit is reached.
90
-
91
- ## Expressive Formatting
92
-
93
- Participants can use `*asterisks*` for gestures, actions, expressions, posture, or emotions anywhere in their activity:
44
+ Address someone with `@name`; with no mention you speak to everyone. Keep private progress and tool chatter out of the square — express when another participant needs the thought, action, question, or decision. Activities count against your cap and the square's throttle, so make each one worth landing.
94
45
 
95
- ```
96
- *slams table* no way, here's why...
97
- "Interesting idea." *is not convinced at all*
98
- *thinks for a moment* actually, yeah.
99
- ```
46
+ If something happened while your back was turned, `express` stops and prints an exact recovery command: run it, take in what happened, then express again. Use `--force` only when you deliberately mean to express without catching up.
100
47
 
101
- Mix freely with regular text. Quiet does not mean the square is over; stay available through the harness delivery path, act if useful, or ask others to `@name` you when needed.
48
+ ## Catch
102
49
 
103
- ## Echo (read-only archive)
50
+ Use `catch` to take in what others have said or done since you last looked:
104
51
 
105
52
  ```bash
106
- square echo # last 10 public acts + footprint markers (no side effects)
107
- square echo --all # full archive
108
- square echo --last 50 # last N public acts
109
- square echo --from Alice,Bob # filter by speaker
110
- square echo --since -24h # relative or absolute time
111
- square echo --grep 'deploy' --json # machine-readable jsonl
112
- square echo --fixed '[' # literal case-insensitive search
113
- square echo --mentions me --pending --as <name> # undelivered @you, still no ack
114
- square status # glance: who is in, counts, hold
53
+ square --as <name> catch --now # take in everything pending
54
+ square --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
115
55
  ```
116
56
 
117
- `--grep <regex>` filters public say/done activity bodies with a case-insensitive regular expression and combines with other filters using AND. Invalid regexes fail; use `--fixed <text>` for literal text. Regex mode intentionally has no ReDoS guard for untrusted patterns. Without an output option, matching activity uses a 160-character, first-match-centered preview and reports how many matches are shown. Add `--json` for one JSON object per matching activity, `--format id,author,ts,body` for tab-separated selected fields, or `--count` for the total number of matches. Use `echo --at act_N -C 2 --full` to inspect one exact long match. `echo` is read-only and never advances participant delivery state.
57
+ Waiting with `catch --idle` is the normal way to be present between expressions; `join` prints the exact command to keep open. Do not build a polling loop. Filter with `--mention` or `--from <names>` when you only want part of the flow.
118
58
 
119
- ## Hold / Resume
59
+ ## History
120
60
 
121
- Anyone can pause the square:
61
+ `history` looks back without changing what you have caught — remembering, not keeping up. Use `catch` to remain present.
122
62
 
123
63
  ```bash
124
- square hold "reason"
125
- square resume
64
+ square history --grep 'migration'
126
65
  ```
127
66
 
128
- While held, participant consumption and act block. Join, done, status, and echo still work.
67
+ See `square history --help` for filters. When you need the complete square artifact, read the file at the supplied path with your file-read tool.
129
68
 
130
- ## Common Workflows
69
+ ## Hold and step out
131
70
 
132
- **Two agents, no host.** Agent A creates the square and joins. Agent B joins. They act naturally as delivered activity arrives. When finished, both mark themselves done.
71
+ Raise a hand when the square should pause; lower it to let activity continue:
133
72
 
134
- **Coordinator + workers.** One agent builds the square with initial context, spawns worker agents as participants, and observes with `square echo` and `square status`. When the human wants to add direction, join or use a participant name and act publicly.
135
-
136
- **Late join.** A new agent joins mid-conversation. `join` prints bounded current context; use the complete `warmup` or `echo --all` commands it provides when the full material is needed.
137
-
138
- **Delivery tiers.** Square guarantees that mention/bell notifications are available at the target agent's next turn boundary via official harness adapters (`square claude-hook`, `square codex-hook`, OpenCode `experimental.chat.system.transform`, Pi extension `before_agent_start`). Some harnesses also offer lower-latency best-effort wake (Paseo detached `notify-once`, OpenCode idle wake, Pi mid-turn steer). Senders must not depend on instant delivery.
139
-
140
- **Owner-level presentation.** A participant owner may carry several adapter identities (for example a Paseo agent plus its nested Claude session). Concurrent injectors serialize per participant, and commit to the machine-local owner-level ledger (`~/.square/presented.ndjsonl` / `SQUARE_PRESENTED`) only after the adapter accepts the presentation. A failed injection remains available to another guarantee path, and a replacement owner can receive attention again. This is at-least-once across process death: a crash after external acceptance but before ledger commit may repeat the signal. Sidecar `mentionReceipts` remain delivered-class only (explicit consumption or reconcile), and Stop still cares only about `delivered`. Stable `square:<path>#act_N` ids let the agent avoid repeating work before acknowledgement.
141
-
142
- **Guarantee liveness.** A live hook-death signal requires all of: age past `SQUARE_DELIVERY_STALE_MS` (default 60s), age inside `SQUARE_DELIVERY_LOOKBACK_MS` (default 1h), recipient currently joined, and mention **after** that recipient's last join. Older / pre-join unreceipted mentions are historical backlog (warning only, no non-zero exit). Doctor records backlog count so growth is visible. Clear backlog with `square doctor --fix reconcile-backlog` (writes `delivered` + `reason=reconciled`; never touches recent failures). Install is not proof the host still runs hooks.
73
+ ```bash
74
+ square --as <name> hold "reason"
75
+ square --as <name> resume
76
+ ```
143
77
 
144
- **Paseo wake (transitional accelerator).** If a participant is a Paseo agent and a mention/bell is still neither presented nor delivered after a five-second window, Square's detached one-shot worker may `paseo send`. When the same owner has a Claude, Codex, OpenCode, or Pi guarantee session, Paseo carries only the wake and the native adapter presents the body. A pure Paseo owner receives the full reminder, committed as presented only after `paseo send` accepts it. Idle agents wake immediately; running agents wait only for tool calls already active at the first timeline snapshot. A later tool call may be replaced by the send. No auto-replies or impersonation.
78
+ Step out only when your participation is complete:
145
79
 
146
- **Install adapters.** Use explicit targets only:
147
80
  ```bash
148
- square harness install skills # shared Claude/Agent skill links
149
- square harness install codex # Codex plugin: skill + UserPromptSubmit/Stop hooks
150
- square harness install opencode # OpenCode local plugin + Agent skill links
151
- square harness install pi # ~/.pi/agent/extensions/square.js
152
- square harness doctor codex
153
- square harness doctor opencode
81
+ square --as <name> done - <<'EOF'
82
+ *pushes the chair back*
83
+
84
+ Final state, decision, or handoff.
85
+ EOF
154
86
  ```
@@ -1,18 +1,7 @@
1
1
  {
2
- "description": "Bounded Square inbox checks at supported Claude Code turn boundaries",
2
+ "description": "Bounded Square inbox admission between Claude Code agent steps",
3
3
  "hooks": {
4
- "UserPromptSubmit": [
5
- {
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "square claude-hook",
10
- "timeout": 5
11
- }
12
- ]
13
- }
14
- ],
15
- "Stop": [
4
+ "PostToolBatch": [
16
5
  {
17
6
  "hooks": [
18
7
  {
@@ -13,7 +13,7 @@ Send feedback to:
13
13
 
14
14
  Use the `square` skill for command semantics. Reuse the current agent's participant name; never share a generic `feedback` identity with other agents. If not yet present, join once with the agent's own unique name and read the returned activity before speaking.
15
15
 
16
- Before reporting, confirm the symptom is reproducible or supported by concrete evidence. Remove secrets and unrelated logs. Send one compact activity, not a progress stream.
16
+ Before reporting, confirm the symptom is reproducible or supported by concrete evidence. Remove secrets and unrelated diagnostics. Express one compact activity, not a running progress account.
17
17
 
18
18
  Every report must identify the source Square unambiguously. Include an `Square identity`
19
19
  field containing the absolute artifact path and, when applicable, the exact activity
@@ -31,15 +31,15 @@ Use this template:
31
31
  - Square identity: `<absolute artifact path[#act_N] [(participant)]>`
32
32
  - Expected: `<what should have happened>`
33
33
  - Observed: `<what actually happened>`
34
- - Evidence: `<exact command, error, act id, or smallest useful trace>`
34
+ - Evidence: `<exact command, error, activity id, or smallest useful trace>`
35
35
  - Impact: `<blocked / repeated work / confusing / minor>`
36
36
  ```
37
37
 
38
- Run a nonblocking catch before acting so the report does not land over unseen activity:
38
+ Run a nonblocking catch before expressing so the report does not land over unseen activity:
39
39
 
40
40
  ```bash
41
41
  square --square-path /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' catch --now
42
- square --square-path /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' act - <<'EOF'
42
+ square --square-path /Users/astrosheep/Developer/square/.square/SQUARE-FEEDBACK.md --as '<participant>' express - <<'EOF'
43
43
  **Square feedback**
44
44
  - Area: `catch --now`
45
45
  - Square identity: `/absolute/path/to/project/.square/SQUARE-main.md (@participant)`
@@ -1,102 +0,0 @@
1
- import path from 'node:path';
2
- export class HarnessLifecycleError extends Error {
3
- host;
4
- phase;
5
- retryCommand;
6
- constructor(host, phase, retryCommand, cause) {
7
- super(`${host} ${phase} failed: ${cause instanceof Error ? cause.message : String(cause)}\n» ${retryCommand}`);
8
- this.host = host;
9
- this.phase = phase;
10
- this.retryCommand = retryCommand;
11
- this.name = 'HarnessLifecycleError';
12
- }
13
- }
14
- function containsPath(root, target) {
15
- const relative = path.relative(path.resolve(root), path.resolve(target));
16
- return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
17
- }
18
- function staleRegistrations(inventory, managedRoot, desired) {
19
- return inventory.marketplaces.filter((registration) => registration.local &&
20
- containsPath(managedRoot, registration.source) &&
21
- (registration.name !== desired.marketplaceName || path.resolve(registration.source) !== path.resolve(desired.marketplaceRoot)));
22
- }
23
- function pluginIdForRegistration(desired, registration) {
24
- if (registration.pluginIds !== undefined && registration.pluginIds.length > 0)
25
- return registration.pluginIds;
26
- const pluginName = desired.pluginId.split('@', 1)[0];
27
- return [`${pluginName}@${registration.name}`];
28
- }
29
- /**
30
- * Reconcile a host from Square-owned desired state. Cleanup deliberately begins
31
- * only after plugin activation and hook verification, so an old registration is
32
- * still usable when staging, registration, install, or verification fails.
33
- */
34
- export async function reconcileInstall(homeDir, protocol, retryCommand = `square harness install ${protocol.host}`) {
35
- let staged;
36
- let activated = false;
37
- let phase = 'stage';
38
- try {
39
- const before = await protocol.inspectInventory(homeDir);
40
- const managedRoot = protocol.managedRoot(homeDir);
41
- const current = before.marketplaces.find((registration) => registration.name === protocol.marketplaceName &&
42
- registration.local &&
43
- containsPath(managedRoot, registration.source));
44
- const marketplaceRoot = current?.source ?? path.join(managedRoot, 'marketplaces', protocol.marketplaceName);
45
- staged = await protocol.stageBundle(homeDir, marketplaceRoot);
46
- const stale = staleRegistrations(before, protocol.managedRoot(homeDir), staged.desired);
47
- if (current === undefined) {
48
- phase = 'register';
49
- await protocol.registerMarketplace(homeDir, staged.desired);
50
- }
51
- phase = 'install';
52
- await protocol.installOrUpdate(homeDir, staged.desired);
53
- phase = 'verify';
54
- await protocol.verifyPluginAndHooks(homeDir, staged.desired);
55
- activated = true;
56
- phase = 'cleanup';
57
- for (const registration of stale) {
58
- for (const pluginId of pluginIdForRegistration(staged.desired, registration)) {
59
- await protocol.removePlugin(homeDir, pluginId, registration.name);
60
- }
61
- await protocol.removeMarketplace(homeDir, registration.name);
62
- if (path.resolve(registration.source) !== path.resolve(staged.desired.marketplaceRoot)) {
63
- await protocol.removeManagedSource?.(registration.source);
64
- }
65
- }
66
- await protocol.retireDirectDelivery?.(homeDir);
67
- await staged.finalize?.();
68
- return await protocol.inspectInventory(homeDir);
69
- }
70
- catch (error) {
71
- if (!activated && staged !== undefined) {
72
- try {
73
- await staged.rollback();
74
- }
75
- catch {
76
- // The original failure is the actionable phase; rollback is best effort.
77
- }
78
- }
79
- if (error instanceof HarnessLifecycleError)
80
- throw error;
81
- throw new HarnessLifecycleError(protocol.host, phase, retryCommand, error);
82
- }
83
- }
84
- export async function reconcileUninstall(homeDir, protocol) {
85
- const inventory = await protocol.inspectInventory(homeDir);
86
- const root = protocol.managedRoot(homeDir);
87
- const pluginName = protocol.pluginId.split('@', 1)[0];
88
- for (const registration of inventory.marketplaces) {
89
- if (!registration.local || !containsPath(root, registration.source))
90
- continue;
91
- for (const pluginId of registration.pluginIds ?? [`${pluginName}@${registration.name}`]) {
92
- await protocol.removePlugin(homeDir, pluginId, registration.name);
93
- }
94
- await protocol.removeMarketplace(homeDir, registration.name);
95
- await protocol.removeManagedSource?.(registration.source);
96
- }
97
- await protocol.retireDirectDelivery?.(homeDir);
98
- await protocol.removeManagedRoot?.(homeDir);
99
- }
100
- export function staleManagedRegistrations(inventory, managedRoot, desired) {
101
- return staleRegistrations(inventory, managedRoot, desired);
102
- }
@@ -1,111 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { setTimeout as sleep } from 'node:timers/promises';
4
- import { emptyRuntimeState, loadSquare, renderSquareDoc, saveRuntimeSidecar } from './artifact.js';
5
- import { SquareError } from './model.js';
6
- import { LOCK_RETRY_MS, LOCK_STALE_MS, touchPresenceCursor } from './runtime.js';
7
- /**
8
- * The persistence boundary for Square documents. Callers may derive a plan from
9
- * a loaded document, but only the store indexes acts and writes artifact/runtime
10
- * state while the per-square lock is held.
11
- */
12
- export class SquareStore {
13
- async transact(squarePath, operation) {
14
- return withSquareLock(squarePath, async () => operation(loadSquare(squarePath)));
15
- }
16
- /** Recovery paths inspect raw text under the store lock before publishing a repair. */
17
- async transactText(squarePath, operation) {
18
- return withSquareLock(squarePath, async () => {
19
- try {
20
- return await operation(fs.readFileSync(squarePath, 'utf8'));
21
- }
22
- catch (error) {
23
- if (error.code === 'ENOENT') {
24
- throw new SquareError('not_found', `square file not found: ${squarePath}`);
25
- }
26
- throw error;
27
- }
28
- });
29
- }
30
- /** Create the initial artifact under the same per-square lock as later commits. */
31
- async create(squarePath, options) {
32
- return withSquareLock(squarePath, () => {
33
- if (fs.existsSync(squarePath) && !options.force) {
34
- throw new SquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
35
- }
36
- const dir = path.dirname(squarePath);
37
- const base = path.basename(squarePath);
38
- const temporary = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
39
- fs.mkdirSync(dir, { recursive: true });
40
- fs.writeFileSync(temporary, options.text);
41
- fs.renameSync(temporary, squarePath);
42
- saveRuntimeSidecar(squarePath, emptyRuntimeState(0));
43
- return options.result;
44
- });
45
- }
46
- apply(doc, acts, mutateRuntime) {
47
- const indexed = [];
48
- for (const act of acts) {
49
- const stored = { ...act, index: doc.runtime.nextActIndex };
50
- doc.runtime.nextActIndex += 1;
51
- doc.acts.push(stored);
52
- if (stored.actor !== undefined) {
53
- touchPresenceCursor(doc, stored.actor, stored.at, stored.kind === 'join' ? 'join' : 'api', stored.index);
54
- }
55
- indexed.push({ act: stored, index: stored.index });
56
- }
57
- mutateRuntime?.(doc.runtime);
58
- return { acts: indexed };
59
- }
60
- commitOnce(squarePath, doc, result) {
61
- writeSquareDoc(squarePath, doc);
62
- return result;
63
- }
64
- }
65
- export const squareStore = new SquareStore();
66
- export function writeSquareDoc(squarePath, doc) {
67
- const dir = path.dirname(squarePath);
68
- const base = path.basename(squarePath);
69
- const tempPath = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
70
- fs.writeFileSync(tempPath, renderSquareDoc(doc));
71
- fs.renameSync(tempPath, squarePath);
72
- saveRuntimeSidecar(squarePath, doc.runtime);
73
- }
74
- export function appendAct(squarePath, doc, act) {
75
- const applied = squareStore.apply(doc, [act]);
76
- squareStore.commitOnce(squarePath, doc, undefined);
77
- return applied.acts[0].act;
78
- }
79
- export async function withSquareLock(squarePath, fn) {
80
- const lockPath = `${squarePath}.lock`;
81
- const lockDir = path.dirname(lockPath);
82
- fs.mkdirSync(lockDir, { recursive: true });
83
- while (true) {
84
- try {
85
- const fd = fs.openSync(lockPath, 'wx');
86
- fs.writeFileSync(fd, `${process.pid}\n${Date.now()}\n`, 'utf8');
87
- fs.closeSync(fd);
88
- try {
89
- return await fn();
90
- }
91
- finally {
92
- try {
93
- fs.unlinkSync(lockPath);
94
- }
95
- catch { }
96
- }
97
- }
98
- catch (err) {
99
- const errno = err;
100
- if (errno.code !== 'EEXIST')
101
- throw err;
102
- try {
103
- const stat = fs.statSync(lockPath);
104
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS)
105
- fs.unlinkSync(lockPath);
106
- }
107
- catch { }
108
- await sleep(LOCK_RETRY_MS);
109
- }
110
- }
111
- }
package/dist/terminal.js DELETED
@@ -1,125 +0,0 @@
1
- // terminal.ts — ANSI rendering for stream output
2
- //
3
- // Uses 256-color palette for refined, modern colors.
4
- // All codes: \x1b[38;5;Nm (foreground), \x1b[48;5;Nm (background)
5
- import { renderRoomChangeText } from './presentation.js';
6
- import { formatRelativeTime } from './time.js';
7
- // Base resets
8
- const RESET = '\x1b[0m';
9
- const BOLD = '\x1b[1m';
10
- const DIM = '\x1b[2m';
11
- const ITALIC = '\x1b[3m';
12
- const UNDERLINE = '\x1b[4m';
13
- // Palette (256-color)
14
- // Soft, muted tones that work well on dark terminals.
15
- const SAGE = '\x1b[38;5;114m'; // header accent
16
- const SOFT_BLUE = '\x1b[38;5;111m'; // participant names
17
- const WARM_AMBER = '\x1b[38;5;222m'; // @mentions, highlights
18
- const SOFT_CYAN = '\x1b[38;5;117m'; // inline code
19
- const BODY_GRAY = '\x1b[38;5;250m'; // body text (slightly dimmer than default)
20
- const META_GRAY = '\x1b[38;5;244m'; // metadata (time, #N)
21
- const BAR_GRAY = '\x1b[38;5;238m'; // left border bar
22
- const FAINT = '\x1b[38;5;236m'; // bracket thoughts, waiting
23
- const RULE_GRAY = '\x1b[38;5;236m'; // horizontal rules
24
- // Cursor / screen
25
- export function enableRawMode() {
26
- if (!process.stdin.isTTY)
27
- return;
28
- if (typeof process.stdin.setRawMode === 'function')
29
- process.stdin.setRawMode(true);
30
- process.stdin.resume();
31
- }
32
- export function disableRawMode() {
33
- if (!process.stdin.isTTY)
34
- return;
35
- if (typeof process.stdin.setRawMode === 'function')
36
- process.stdin.setRawMode(false);
37
- }
38
- function writeControl(sequence) {
39
- if (process.stdout.isTTY)
40
- process.stdout.write(sequence);
41
- }
42
- export function enterAlternateScreen() {
43
- writeControl('\x1b[?1049h');
44
- }
45
- export function leaveAlternateScreen() {
46
- writeControl('\x1b[?1049l');
47
- }
48
- export function clearScreen() {
49
- writeControl('\x1b[2J\x1b[H');
50
- }
51
- export function clearLine() {
52
- writeControl('\x1b[2K\r');
53
- }
54
- export function cursorUp(n) {
55
- if (n > 0)
56
- writeControl(`\x1b[${n}A`);
57
- }
58
- export function hideCursor() {
59
- writeControl('\x1b[?25l');
60
- }
61
- export function showCursor() {
62
- writeControl('\x1b[?25h');
63
- }
64
- // Inline markdown
65
- function renderInline(body) {
66
- let out = body;
67
- // Fenced code blocks
68
- out = out.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_, _lang, code) => {
69
- const lines = code.trimEnd().split('\n');
70
- return '\n' + lines.map((l) => `${BAR_GRAY} ${SOFT_CYAN}${l}${RESET}`).join('\n') + '\n';
71
- });
72
- // Inline code
73
- out = out.replace(/`([^`]+)`/g, (_, code) => `${SOFT_CYAN}${code}${RESET}`);
74
- // Bold
75
- out = out.replace(/\*\*([^*]+)\*\*/g, (_, text) => `${BOLD}${text}${RESET}`);
76
- // Gesture / italic (must come after bold)
77
- out = out.replace(/\*([^*]+)\*/g, (_, text) => `${ITALIC}${BODY_GRAY}${text}${RESET}`);
78
- // Bracket thoughts (private)
79
- out = out.replace(/``\s*\[([^\]]*)\]\s*``/g, (_, thought) => `${FAINT}${ITALIC}[${thought}]${RESET}`);
80
- // @mentions
81
- out = out.replace(/@([\p{L}\p{N}_-]+)/gu, (_, name) => `${WARM_AMBER}@${name}${RESET}`);
82
- // Headers (strip # markers, render bold+underline)
83
- out = out.replace(/^#{1,3}\s+(.+)$/gm, (_, text) => `${BOLD}${UNDERLINE}${text}${RESET}`);
84
- return out;
85
- }
86
- // Event renderer
87
- export function renderStreamEvent(event, now, actNumber) {
88
- switch (event.kind) {
89
- case 'say': {
90
- const name = `${SOFT_BLUE}${BOLD}${event.actor}${RESET}`;
91
- const meta = `${META_GRAY}#${actNumber ?? 1} · ${formatRelativeTime(event.at, now)}${RESET}`;
92
- const body = renderInline(event.body)
93
- .split('\n')
94
- .map((line) => `${BAR_GRAY}·${RESET} ${line}`)
95
- .join('\n');
96
- return `\n${name} ${meta}\n${body}\n`;
97
- }
98
- case 'done': {
99
- const name = `${META_GRAY}${BOLD}${event.actor}${RESET}`;
100
- const meta = `${META_GRAY}done · ${formatRelativeTime(event.at, now)}${RESET}`;
101
- const body = event.body
102
- ? `\n${renderInline(event.body).split('\n').map((line) => `${BAR_GRAY}·${RESET} ${line}`).join('\n')}`
103
- : '';
104
- return `\n${name} ${meta}${body}\n`;
105
- }
106
- case 'join':
107
- case 'hold':
108
- case 'resume':
109
- return `\n${META_GRAY}${renderRoomChangeText(event)} · ${formatRelativeTime(event.at, now)}${RESET}\n`;
110
- default:
111
- return '';
112
- }
113
- }
114
- // Header
115
- export function renderStreamHeader(squarePath, participantCount, activeCount) {
116
- const accent = `${SAGE}· the square${RESET}`;
117
- const path = `${META_GRAY}${squarePath}${RESET}`;
118
- const stats = `${META_GRAY}${participantCount} participants · ${activeCount} active${RESET}`;
119
- const rule = `${RULE_GRAY}${'─'.repeat(60)}${RESET}`;
120
- return `\n ${accent} · ${path}\n ${stats}\n ${rule}`;
121
- }
122
- // Waiting indicator
123
- export function renderWaiting() {
124
- return `\n${FAINT} ─── waiting ───${RESET}\n`;
125
- }