pi-link 0.1.5 → 0.1.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.
- package/CHANGELOG.md +25 -1
- package/README.md +4 -5
- package/index.ts +22 -117
- package/package.json +4 -1
- package/skills/pi-link-coordination/SKILL.md +114 -0
- package/sync.ffs_db +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-link are documented here.
|
|
4
4
|
|
|
5
|
-
This changelog is based on the git history from `2026-03-21` through `2026-04-
|
|
5
|
+
This changelog is based on the git history from `2026-03-21` through `2026-04-03` (current). Versions correspond to npm publishes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0.1.7 — 2026-04-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Bundled `pi-link-coordination` skill.** The coordination guide is now shipped with the package via `pi.skills` manifest entry. Installing pi-link now auto-loads the skill — no manual copy required. The skill provides on-demand guidance for agents delegating work across terminals: tool selection (`link_prompt` vs `link_send`), the golden rule (no sync-after-async on same target), callback contracts, and coordination modes.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 0.1.6 — 2026-04-03
|
|
18
|
+
|
|
19
|
+
**Pi 0.65.0 migration.** Pi removed `session_switch` and `session_fork` events. All session transitions (startup, reload, `/new`, `/resume`, `/fork`) now fire `session_start` with `event.reason`. Each transition tears down the old extension runtime via `session_shutdown` before creating a fresh one — so there is no live connection to update in-place across sessions.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Persistent connection intent.** `/link-connect` and `/link-disconnect` now save their state to the session via `pi.appendEntry("link-active", ...)`. On `session_start`, the saved preference is checked before falling back to `--link`. Connect once and it stays connected across session resumes without needing the flag. Explicit user intent (`link-active`) takes precedence over the `--link` flag default.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
- **`cwd_update` message type.** With the old `session_switch` gone, mid-session cwd changes have no trigger. Working directories are now only reported on connect (via `register`/`welcome`). Protocol returns to 9 message types.
|
|
28
|
+
|
|
29
|
+
- **`session_switch` handler.** The 77-line in-place mutation matrix (hub rename, cwd diffing, client reconnect) is dead under the new lifecycle. Replaced by a unified `session_start` handler + `shouldConnect()` helper.
|
|
6
30
|
|
|
7
31
|
---
|
|
8
32
|
|
package/README.md
CHANGED
|
@@ -191,7 +191,7 @@ Send a prompt to a remote terminal and **wait** for the LLM's response (synchron
|
|
|
191
191
|
|
|
192
192
|
Lists all connected terminals with role info, live agent status, working directory, and self-identification. Takes no parameters.
|
|
193
193
|
|
|
194
|
-
Each terminal reports its current working directory on connect
|
|
194
|
+
Each terminal reports its current working directory on connect. `link_list` shows the full absolute path so agents can choose the right target, use explicit paths when terminals differ, and catch wrong-project mistakes early.
|
|
195
195
|
|
|
196
196
|
Each terminal's status is derived automatically from Pi lifecycle events — agents can't set it manually. Three states:
|
|
197
197
|
|
|
@@ -397,7 +397,7 @@ The `pi.extensions` field tells Pi which files to load as extensions. Here it po
|
|
|
397
397
|
|
|
398
398
|
### Protocol
|
|
399
399
|
|
|
400
|
-
The wire protocol consists of **
|
|
400
|
+
The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames. Cwd-related fields are optional for backward compatibility.
|
|
401
401
|
|
|
402
402
|
| Type | Direction | Purpose |
|
|
403
403
|
| ----------------- | --------------- | ----------------------------------------------------------------------- |
|
|
@@ -409,7 +409,6 @@ The wire protocol consists of **10 message types**, all serialized as JSON over
|
|
|
409
409
|
| `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
|
|
410
410
|
| `prompt_response` | Any → Any | Response carrying the remote prompt result |
|
|
411
411
|
| `status_update` | Any → Hub → All | Terminal broadcasts its agent status change |
|
|
412
|
-
| `cwd_update` | Any → Hub → All | Terminal broadcasts a cwd change |
|
|
413
412
|
| `error` | Hub → Client | Error notification |
|
|
414
413
|
|
|
415
414
|
### Message Flow Examples
|
|
@@ -429,7 +428,7 @@ Client Hub
|
|
|
429
428
|
| |
|
|
430
429
|
```
|
|
431
430
|
|
|
432
|
-
Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes status and cwd snapshots for all connected terminals (fields omitted above for brevity). `terminal_joined` also includes the new terminal's optional cwd
|
|
431
|
+
Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes status and cwd snapshots for all connected terminals (fields omitted above for brevity). `terminal_joined` also includes the new terminal's optional cwd.
|
|
433
432
|
|
|
434
433
|
**Sending a chat message:**
|
|
435
434
|
|
|
@@ -482,7 +481,7 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
|
|
|
482
481
|
| `agentRunning` | `boolean` | Whether an agent run is active; blocks incoming remote prompts |
|
|
483
482
|
| `activeToolName` | `string \| null` | Name of the currently executing tool (drives `tool:<name>` status) |
|
|
484
483
|
| `stateSince` | `number` | Timestamp of last status change (used for duration display) |
|
|
485
|
-
| `currentCwd` | `string` | Current working directory reported to peers on connect
|
|
484
|
+
| `currentCwd` | `string` | Current working directory reported to peers on connect |
|
|
486
485
|
| `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
|
|
487
486
|
| `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
|
|
488
487
|
| `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses (includes inactivity + ceiling timers per entry) |
|
package/index.ts
CHANGED
|
@@ -49,11 +49,6 @@ interface TerminalJoinedMsg {
|
|
|
49
49
|
terminals: string[];
|
|
50
50
|
cwd?: string;
|
|
51
51
|
}
|
|
52
|
-
interface CwdUpdateMsg {
|
|
53
|
-
type: "cwd_update";
|
|
54
|
-
name: string;
|
|
55
|
-
cwd: string;
|
|
56
|
-
}
|
|
57
52
|
interface TerminalLeftMsg {
|
|
58
53
|
type: "terminal_left";
|
|
59
54
|
name: string;
|
|
@@ -105,7 +100,6 @@ type LinkMessage =
|
|
|
105
100
|
| PromptRequestMsg
|
|
106
101
|
| PromptResponseMsg
|
|
107
102
|
| StatusUpdateMsg
|
|
108
|
-
| CwdUpdateMsg
|
|
109
103
|
| ErrorMsg;
|
|
110
104
|
|
|
111
105
|
// ─── Extension ───────────────────────────────────────────────────────────────
|
|
@@ -240,17 +234,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
234
|
return normalized;
|
|
241
235
|
}
|
|
242
236
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
237
|
+
// ── Connection intent ──────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function shouldConnect(_ctx: ExtensionContext): boolean {
|
|
240
|
+
const saved = _ctx.sessionManager
|
|
241
|
+
.getEntries()
|
|
242
|
+
.filter(
|
|
243
|
+
(e: { type: string; customType?: string }) =>
|
|
244
|
+
e.type === "custom" && e.customType === "link-active",
|
|
245
|
+
)
|
|
246
|
+
.pop() as { data?: { active?: boolean } } | undefined;
|
|
247
|
+
if (saved?.data?.active !== undefined) return saved.data.active;
|
|
248
|
+
return pi.getFlag("link") === true;
|
|
254
249
|
}
|
|
255
250
|
|
|
256
251
|
// ── Pending prompt helpers ───────────────────────────────────────────────
|
|
@@ -452,10 +447,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
452
447
|
resetInactivityFor(msg.name);
|
|
453
448
|
break;
|
|
454
449
|
|
|
455
|
-
case "cwd_update":
|
|
456
|
-
terminalCwds.set(msg.name, msg.cwd);
|
|
457
|
-
break;
|
|
458
|
-
|
|
459
450
|
// ── Chat message ──
|
|
460
451
|
case "chat":
|
|
461
452
|
pi.sendMessage(
|
|
@@ -588,21 +579,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
588
579
|
return;
|
|
589
580
|
}
|
|
590
581
|
|
|
591
|
-
// Cwd update — store and relay to other clients only
|
|
592
|
-
if (msg.type === "cwd_update") {
|
|
593
|
-
hubTerminalCwds.set(clientName, msg.cwd);
|
|
594
|
-
const normalized: CwdUpdateMsg = {
|
|
595
|
-
type: "cwd_update",
|
|
596
|
-
name: clientName,
|
|
597
|
-
cwd: msg.cwd,
|
|
598
|
-
};
|
|
599
|
-
const json = JSON.stringify(normalized);
|
|
600
|
-
for (const [otherWs, name] of hubClients) {
|
|
601
|
-
if (name !== clientName) otherWs.send(json);
|
|
602
|
-
}
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
582
|
// Route chat / prompt messages
|
|
607
583
|
if (
|
|
608
584
|
msg.type === "chat" ||
|
|
@@ -817,90 +793,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
817
793
|
terminalName = preferredName;
|
|
818
794
|
}
|
|
819
795
|
|
|
820
|
-
if (
|
|
796
|
+
if (shouldConnect(_ctx)) await initialize();
|
|
821
797
|
});
|
|
822
798
|
|
|
823
799
|
pi.on("session_shutdown", async () => {
|
|
824
800
|
cleanup();
|
|
825
801
|
});
|
|
826
802
|
|
|
827
|
-
pi.on("session_switch", async (_event, _ctx) => {
|
|
828
|
-
ctx = _ctx;
|
|
829
|
-
|
|
830
|
-
// 1. Cwd change detection (always, before any name logic)
|
|
831
|
-
const newCwd = _ctx.cwd;
|
|
832
|
-
const cwdChanged = newCwd !== currentCwd;
|
|
833
|
-
if (cwdChanged) currentCwd = newCwd;
|
|
834
|
-
|
|
835
|
-
// 2. Restore preferred name from the new session
|
|
836
|
-
const saved = _ctx.sessionManager
|
|
837
|
-
.getEntries()
|
|
838
|
-
.filter(
|
|
839
|
-
(e: { type: string; customType?: string }) =>
|
|
840
|
-
e.type === "custom" && e.customType === "link-name",
|
|
841
|
-
)
|
|
842
|
-
.pop() as { data?: { name?: string } } | undefined;
|
|
843
|
-
|
|
844
|
-
preferredName = saved?.data?.name ?? null;
|
|
845
|
-
const desiredName = preferredName ?? `t-${crypto.randomUUID().slice(0, 4)}`;
|
|
846
|
-
const nameChanged = desiredName !== terminalName;
|
|
847
|
-
|
|
848
|
-
if (!nameChanged && !cwdChanged) return; // nothing to do
|
|
849
|
-
|
|
850
|
-
if (!nameChanged) {
|
|
851
|
-
// Name stayed the same, but cwd changed — push cwd update
|
|
852
|
-
pushCwdUpdate();
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// Name changed (cwd may or may not have changed too)
|
|
857
|
-
if (role === "hub") {
|
|
858
|
-
// Hub rename in-place — avoid tearing down the server
|
|
859
|
-
const takenByOther = Array.from(hubClients.values()).includes(
|
|
860
|
-
desiredName,
|
|
861
|
-
);
|
|
862
|
-
if (takenByOther) {
|
|
863
|
-
// Can't use preferred name — keep current identity
|
|
864
|
-
ctx?.ui.notify(
|
|
865
|
-
`Session preferred name "${desiredName}" is taken, keeping "${terminalName}"`,
|
|
866
|
-
"warning",
|
|
867
|
-
);
|
|
868
|
-
// Still push cwd update under current name if cwd changed
|
|
869
|
-
if (cwdChanged) pushCwdUpdate();
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
const old = terminalName;
|
|
873
|
-
terminalName = desiredName;
|
|
874
|
-
const list = terminalList();
|
|
875
|
-
connectedTerminals = list;
|
|
876
|
-
updateStatus();
|
|
877
|
-
// Notify clients only — hub already updated local state
|
|
878
|
-
hubBroadcast(
|
|
879
|
-
{ type: "terminal_left", name: old, terminals: list },
|
|
880
|
-
terminalName,
|
|
881
|
-
);
|
|
882
|
-
hubBroadcast(
|
|
883
|
-
{
|
|
884
|
-
type: "terminal_joined",
|
|
885
|
-
name: desiredName,
|
|
886
|
-
terminals: list,
|
|
887
|
-
cwd: currentCwd,
|
|
888
|
-
},
|
|
889
|
-
terminalName,
|
|
890
|
-
);
|
|
891
|
-
pushStatus(true);
|
|
892
|
-
} else if (role === "client") {
|
|
893
|
-
// Client — disconnect and reconnect with new name (register includes cwd)
|
|
894
|
-
terminalName = desiredName;
|
|
895
|
-
disconnect();
|
|
896
|
-
manuallyDisconnected = false;
|
|
897
|
-
await initialize();
|
|
898
|
-
} else {
|
|
899
|
-
// Disconnected — just update local name
|
|
900
|
-
terminalName = desiredName;
|
|
901
|
-
}
|
|
902
|
-
});
|
|
903
|
-
|
|
904
803
|
pi.on("agent_start", async () => {
|
|
905
804
|
agentRunning = true;
|
|
906
805
|
activeToolName = null;
|
|
@@ -1384,18 +1283,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1384
1283
|
pi.registerCommand("link-disconnect", {
|
|
1385
1284
|
description: "Disconnect from the link",
|
|
1386
1285
|
handler: async (_args, _ctx) => {
|
|
1286
|
+
pi.appendEntry("link-active", { active: false });
|
|
1287
|
+
manuallyDisconnected = true;
|
|
1387
1288
|
if (role === "disconnected") {
|
|
1388
|
-
|
|
1289
|
+
if (reconnectTimer) {
|
|
1290
|
+
clearTimeout(reconnectTimer);
|
|
1291
|
+
reconnectTimer = null;
|
|
1292
|
+
}
|
|
1293
|
+
_ctx.ui.notify("Link disconnected", "info");
|
|
1389
1294
|
return;
|
|
1390
1295
|
}
|
|
1391
|
-
manuallyDisconnected = true;
|
|
1392
1296
|
disconnect();
|
|
1393
1297
|
_ctx.ui.notify("Disconnected from link", "info");
|
|
1394
1298
|
},
|
|
1395
1299
|
});
|
|
1396
1300
|
|
|
1397
1301
|
pi.registerCommand("link-connect", {
|
|
1398
|
-
description: "Connect to the link
|
|
1302
|
+
description: "Connect to the link",
|
|
1399
1303
|
handler: async (_args, _ctx) => {
|
|
1400
1304
|
if (role !== "disconnected") {
|
|
1401
1305
|
_ctx.ui.notify(
|
|
@@ -1404,6 +1308,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1404
1308
|
);
|
|
1405
1309
|
return;
|
|
1406
1310
|
}
|
|
1311
|
+
pi.appendEntry("link-active", { active: true });
|
|
1407
1312
|
manuallyDisconnected = false;
|
|
1408
1313
|
await initialize();
|
|
1409
1314
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-link",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "WebSocket-based inter-terminal communication for Pi. Connect multiple Pi terminals over a local link network.",
|
|
5
5
|
"author": "alvivar",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"pi": {
|
|
26
26
|
"extensions": [
|
|
27
27
|
"./index.ts"
|
|
28
|
+
],
|
|
29
|
+
"skills": [
|
|
30
|
+
"./skills"
|
|
28
31
|
]
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-link-coordination
|
|
3
|
+
description: Guidance for coordinating work across Pi terminals using pi-link. Use when delegating tasks, choosing between link_prompt and link_send, planning async vs sync work, batching parallel jobs, or avoiding busy/conflict patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pi-Link Coordination
|
|
7
|
+
|
|
8
|
+
How to coordinate work across Pi terminals via pi-link.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Tool Selection Rule
|
|
13
|
+
|
|
14
|
+
- Need the answer back now? → `link_prompt`
|
|
15
|
+
- Need autonomous work done? → `link_send(triggerTurn: true)`
|
|
16
|
+
- Need to notify only? → `link_send(triggerTurn: false)`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## The Golden Rule
|
|
21
|
+
|
|
22
|
+
> After `link_send(triggerTurn: true)` to terminal X, do not `link_prompt` X until X sends a completion callback.
|
|
23
|
+
|
|
24
|
+
Pick one mode per terminal per task. Mixing sync and async on the same terminal is the most common coordination failure.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The Tools
|
|
29
|
+
|
|
30
|
+
### `link_list`
|
|
31
|
+
|
|
32
|
+
Returns connected terminals with names, live status (`idle`, `thinking`, `tool:<name>`), and working directory (cwd). Use before delegating when availability or path context is uncertain.
|
|
33
|
+
|
|
34
|
+
### `link_prompt`
|
|
35
|
+
|
|
36
|
+
Synchronous RPC. Send a prompt, wait for the response.
|
|
37
|
+
|
|
38
|
+
- Fails immediately if target is missing, self, disconnects, or busy (local work or another remote prompt)
|
|
39
|
+
- 90s inactivity timeout, 30min hard ceiling
|
|
40
|
+
- Remote agent doesn't share your context — prompts must be self-contained
|
|
41
|
+
- Include: goal, scope, constraints, output format, done condition
|
|
42
|
+
|
|
43
|
+
### `link_send`
|
|
44
|
+
|
|
45
|
+
Fire-and-forget. Send to one terminal or `to: "*"` to broadcast (excludes sender).
|
|
46
|
+
|
|
47
|
+
Set `triggerTurn: true` to activate the receiver's LLM. The sender does **not** get the response back.
|
|
48
|
+
|
|
49
|
+
**Callback contract for `triggerTurn: true`:** ask the receiver to reply via `link_send` with:
|
|
50
|
+
|
|
51
|
+
- `DONE` signal
|
|
52
|
+
- Output paths / artifacts created
|
|
53
|
+
- Blockers or open questions
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Operating Constraints
|
|
58
|
+
|
|
59
|
+
- **One remote prompt at a time per target.** Concurrent requests rejected as busy.
|
|
60
|
+
- **No shared context.** Every remote prompt must be self-contained.
|
|
61
|
+
- **Messages are ephemeral.** Offline terminals lose messages.
|
|
62
|
+
- **Localhost only.** Same machine.
|
|
63
|
+
- **Cwd is a hint, not proof.** Same cwd ≠ same workspace/branch/access. Use explicit paths; absolute when cwds differ or shared-root assumptions are unclear.
|
|
64
|
+
- **Naming:** `role@domain` (e.g., `builder@pi-link`). Only talk to your own domain unless told otherwise.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Coordination Modes
|
|
69
|
+
|
|
70
|
+
### Sync ask — `link_prompt`
|
|
71
|
+
|
|
72
|
+
For answers, review, analysis you need back now. One terminal at a time. Keep scope focused to avoid timeout.
|
|
73
|
+
|
|
74
|
+
### Async delegate — `link_send(triggerTurn: true)`
|
|
75
|
+
|
|
76
|
+
For autonomous work. Require the callback contract (DONE + paths + blockers). Do your own work in parallel. Don't `link_prompt` the target until the callback arrives.
|
|
77
|
+
|
|
78
|
+
### Parallel batch — async to multiple terminals
|
|
79
|
+
|
|
80
|
+
Distribute independent tasks. Use explicit paths (absolute if cwds differ). Wait for all callbacks, then synthesize. Don't prompt any dispatched terminal until its callback arrives.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Anti-Patterns
|
|
85
|
+
|
|
86
|
+
**❌ Mixing async and sync on the same terminal**
|
|
87
|
+
Dispatched with `link_send(triggerTurn: true)` then sent a `link_prompt` → rejected as busy. See Golden Rule.
|
|
88
|
+
|
|
89
|
+
**❌ Using `link_send` when you need the response**
|
|
90
|
+
Result disappears. Use `link_prompt`.
|
|
91
|
+
|
|
92
|
+
**❌ Vague prompts**
|
|
93
|
+
"Fix the bug" is useless. Include file, line, root cause, expected fix.
|
|
94
|
+
|
|
95
|
+
**❌ No completion callback on async work**
|
|
96
|
+
Always require DONE + artifact paths + blockers.
|
|
97
|
+
|
|
98
|
+
**❌ Circular delegation**
|
|
99
|
+
A → B → C → A = deadlock. Maintain clear hierarchy.
|
|
100
|
+
|
|
101
|
+
**❌ Skipping `link_list` before retrying a busy target**
|
|
102
|
+
Check status before re-sending.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Quick Reference
|
|
107
|
+
|
|
108
|
+
| I need to... | Tool | Mode |
|
|
109
|
+
| -------------------------------- | ------------------------------- | --------------- |
|
|
110
|
+
| See who's available | `link_list` | — |
|
|
111
|
+
| Get an answer from another agent | `link_prompt` | Synchronous |
|
|
112
|
+
| Delegate autonomous work | `link_send(triggerTurn: true)` | Asynchronous |
|
|
113
|
+
| Notify without activating | `link_send(triggerTurn: false)` | Fire-and-forget |
|
|
114
|
+
| Broadcast to all | `link_send(to: "*")` | Broadcast |
|
package/sync.ffs_db
CHANGED
|
Binary file
|