@hwip/cli 0.0.1 → 0.1.0
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/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1979 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +292 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +35 -0
- package/dist/blocked-prompt-seal.js +89 -0
- package/dist/blocked-prompt.d.ts +57 -0
- package/dist/blocked-prompt.js +181 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +272 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5680 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* opencode's session store, read into the events the timeline draws.
|
|
4
|
+
*
|
|
5
|
+
* opencode keeps no transcript file. 2.x writes every session to SQLite
|
|
6
|
+
* (`opencode.db`, WAL), one `session_message` row per user message and per
|
|
7
|
+
* assistant step, and rewrites an assistant row in place while it streams. So
|
|
8
|
+
* this file is both halves the other agents split: a reader that asks the
|
|
9
|
+
* database what changed (`readOpencodeTranscript`, the `TranscriptReader`
|
|
10
|
+
* behind the opencode row) and a projector for what it hands back
|
|
11
|
+
* (`projectOpencodeEntries`).
|
|
12
|
+
*
|
|
13
|
+
* Shape measured 2026-10-10 against opencode 2.0.24 — the schema with
|
|
14
|
+
* `.schema`, the JSON with `json_each` key and type counts over 3,965 rows on
|
|
15
|
+
* this machine, and a scratch store written by sessions run for the purpose.
|
|
16
|
+
* No conversation text was opened.
|
|
17
|
+
*
|
|
18
|
+
* - `session_message(id, session_id, type, seq, time_created, time_updated,
|
|
19
|
+
* data)`, `seq` increasing per session. `type` is `user` · `assistant` ·
|
|
20
|
+
* `system` · `synthetic` · `idle` · `compaction` · `*-switched`; only the
|
|
21
|
+
* first two are drawn.
|
|
22
|
+
* - user `data`: `{ text, time: { created }, files, agents, skills }`.
|
|
23
|
+
* - assistant `data`: `{ model: { id, providerID }, content: [...], tokens:
|
|
24
|
+
* { input, output, reasoning, cache: { read, write } }, time: { created,
|
|
25
|
+
* completed?, streamed? }, finish, … }` — `time.completed` is set once the
|
|
26
|
+
* step is over. `content` items are `{ type: "text", text }`,
|
|
27
|
+
* `{ type: "reasoning", … }` and `{ type: "tool", id, name, state: { status,
|
|
28
|
+
* input, content: [{ type: "text", text }], error }, time }`; a finished
|
|
29
|
+
* call is `completed` or `error`.
|
|
30
|
+
* - 1.x wrote `session` · `message` · `part` instead. 2.x leaves those alone
|
|
31
|
+
* and this file does not read them.
|
|
32
|
+
*
|
|
33
|
+
* The lines the reader hands over are `TurnEvent`s already, one JSON object
|
|
34
|
+
* each, so the projector only checks them. The work of deciding what is new
|
|
35
|
+
* lives in `opencodeLinesOf`, which is pure and is where the tests point.
|
|
36
|
+
*/
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.projectOpencodeEntries = exports.readOpencodeTranscript = exports.opencodeLinesOf = exports.opencodeRowsSql = exports.OPENCODE_MIN_QUERY_MS = exports.OPENCODE_OPEN_WINDOW = exports.OPENCODE_ROWS_PER_READ = exports.OPENCODE_BACKFILL_ROWS = exports.opencodeSourcePath = exports.OPENCODE_SESSION_ID = exports.opencodeDbPath = void 0;
|
|
39
|
+
const fs_1 = require("fs");
|
|
40
|
+
const os_1 = require("os");
|
|
41
|
+
const path_1 = require("path");
|
|
42
|
+
const transcript_tail_js_1 = require("./transcript-tail.js");
|
|
43
|
+
/** Where opencode 2.x keeps its store, by the rules opencode itself follows (`opencode debug paths db`, 2.0.24). */
|
|
44
|
+
const opencodeDbPath = (env = process.env) => {
|
|
45
|
+
if (env.OPENCODE_DB && (0, path_1.isAbsolute)(env.OPENCODE_DB))
|
|
46
|
+
return env.OPENCODE_DB;
|
|
47
|
+
const data = env.XDG_DATA_HOME && (0, path_1.isAbsolute)(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : (0, path_1.join)((0, os_1.homedir)(), '.local', 'share');
|
|
48
|
+
return (0, path_1.join)(data, 'opencode', 'opencode.db');
|
|
49
|
+
};
|
|
50
|
+
exports.opencodeDbPath = opencodeDbPath;
|
|
51
|
+
/**
|
|
52
|
+
* A session id as opencode mints them (`ses_` + base62, measured). The query
|
|
53
|
+
* below has nowhere to bind a parameter (`sqliteJson` runs the `sqlite3`
|
|
54
|
+
* binary), so an id goes into the SQL text only after it passes this.
|
|
55
|
+
*/
|
|
56
|
+
exports.OPENCODE_SESSION_ID = /^ses_[A-Za-z0-9]{1,64}$/;
|
|
57
|
+
/**
|
|
58
|
+
* The transcript "path" an opencode pane resolves to: the store and the session
|
|
59
|
+
* in one string. A string because every consumer compares and logs paths, and
|
|
60
|
+
* because the session changing — a new one in the same pane — has to read as
|
|
61
|
+
* the path changing, which is what makes `turn-watch` start over on it.
|
|
62
|
+
*/
|
|
63
|
+
const opencodeSourcePath = (db, sessionId) => `${db}#${sessionId}`;
|
|
64
|
+
exports.opencodeSourcePath = opencodeSourcePath;
|
|
65
|
+
const parseSourcePath = (path) => {
|
|
66
|
+
const at = path.lastIndexOf('#');
|
|
67
|
+
if (at <= 0)
|
|
68
|
+
return null;
|
|
69
|
+
const sessionId = path.slice(at + 1);
|
|
70
|
+
const db = path.slice(0, at);
|
|
71
|
+
// The path also comes back from the recorder's state file, and goes to `sqlite3` as an argument:
|
|
72
|
+
// a relative one could start with `-` and read as an option.
|
|
73
|
+
return exports.OPENCODE_SESSION_ID.test(sessionId) && (0, path_1.isAbsolute)(db) ? { db, sessionId } : null;
|
|
74
|
+
};
|
|
75
|
+
/** A row the query could have returned — anything else is not drawn. */
|
|
76
|
+
const isRow = (value) => {
|
|
77
|
+
if (!value || typeof value !== 'object')
|
|
78
|
+
return false;
|
|
79
|
+
const row = value;
|
|
80
|
+
return typeof row.id === 'string' && typeof row.type === 'string' && typeof row.seq === 'number' && Number.isSafeInteger(row.seq);
|
|
81
|
+
};
|
|
82
|
+
/** Rows read on the first look at a session — enough for the turn in flight and a screen above it. */
|
|
83
|
+
exports.OPENCODE_BACKFILL_ROWS = 200;
|
|
84
|
+
/** Rows read per look after that. */
|
|
85
|
+
exports.OPENCODE_ROWS_PER_READ = 500;
|
|
86
|
+
/** How far behind the newest row an unfinished one may hold the read position. */
|
|
87
|
+
exports.OPENCODE_OPEN_WINDOW = 200;
|
|
88
|
+
/** The fewest milliseconds between two queries for one session. */
|
|
89
|
+
exports.OPENCODE_MIN_QUERY_MS = 1_000;
|
|
90
|
+
/**
|
|
91
|
+
* Which `state.input` key reads as "what this call is about", first match
|
|
92
|
+
* wins. The keys are the ones 2.0.24's tools were seen to take (read, edit,
|
|
93
|
+
* write: `filePath`/`path`; glob, grep: `pattern`; webfetch: `url`; subagent,
|
|
94
|
+
* task: `description`). `command` is absent for the reason it is absent from
|
|
95
|
+
* every other projector — a command line is the argument most likely to carry
|
|
96
|
+
* a secret.
|
|
97
|
+
*/
|
|
98
|
+
const TARGET_KEYS = ['filePath', 'path', 'pattern', 'url', 'description'];
|
|
99
|
+
/** Prose is cut in SQL a little above the event cap, so `clamp` still marks the cut. */
|
|
100
|
+
const SQL_TEXT_CHARS = transcript_tail_js_1.MAX_EVENT_TEXT_CHARS + 1;
|
|
101
|
+
/**
|
|
102
|
+
* The rows of one session from `from` on, or its last `OPENCODE_BACKFILL_ROWS`
|
|
103
|
+
* when `from` is null. Exported for the test that pins the id check.
|
|
104
|
+
*/
|
|
105
|
+
const opencodeRowsSql = (sessionId, from) => {
|
|
106
|
+
if (!exports.OPENCODE_SESSION_ID.test(sessionId))
|
|
107
|
+
throw new Error('not an opencode session id');
|
|
108
|
+
if (from !== null && !Number.isSafeInteger(from))
|
|
109
|
+
throw new Error('not a row position');
|
|
110
|
+
const item = (key) => `json_extract(c.value, '$.${key}')`;
|
|
111
|
+
const target = `substr(coalesce(${TARGET_KEYS.map((k) => item(`state.input.${k}`)).join(', ')}), 1, ${transcript_tail_js_1.MAX_EVENT_FIELD_CHARS + 1})`;
|
|
112
|
+
const lines = `(SELECT sum(length(json_extract(o.value, '$.text')) - length(replace(json_extract(o.value, '$.text'), char(10), '')) + 1)
|
|
113
|
+
FROM json_each(c.value, '$.state.content') o WHERE json_extract(o.value, '$.type') = 'text')`;
|
|
114
|
+
const items = `(SELECT json_group_array(json_object(
|
|
115
|
+
'type', ${item('type')}, 'id', ${item('id')}, 'name', ${item('name')},
|
|
116
|
+
'text', CASE WHEN ${item('type')} = 'text' THEN substr(${item('text')}, 1, ${SQL_TEXT_CHARS}) END,
|
|
117
|
+
'status', ${item('state.status')}, 'target', ${target}, 'lines', ${lines}, 'at', ${item('time.created')}))
|
|
118
|
+
FROM json_each(m.data, '$.content') c)`;
|
|
119
|
+
const select = `SELECT m.id AS id, m.seq AS seq, m.type AS type, m.time_created AS created,
|
|
120
|
+
json_extract(m.data, '$.time.completed') AS completed,
|
|
121
|
+
CASE WHEN m.type = 'user' THEN substr(json_extract(m.data, '$.text'), 1, ${SQL_TEXT_CHARS}) END AS text,
|
|
122
|
+
json_extract(m.data, '$.model.id') AS model,
|
|
123
|
+
json_extract(m.data, '$.tokens.output') AS out,
|
|
124
|
+
json_extract(m.data, '$.tokens.input') AS input,
|
|
125
|
+
json_extract(m.data, '$.tokens.cache.read') AS cacheRead,
|
|
126
|
+
json_extract(m.data, '$.tokens.cache.write') AS cacheWrite,
|
|
127
|
+
json_extract(m.data, '$.error.type') AS errorType,
|
|
128
|
+
substr(json_extract(m.data, '$.error.message'), 1, ${SQL_TEXT_CHARS}) AS errorText,
|
|
129
|
+
CASE WHEN m.type = 'assistant' THEN ${items} END AS items
|
|
130
|
+
FROM session_message m
|
|
131
|
+
WHERE m.session_id = '${sessionId}' AND m.type IN ('user', 'assistant')`;
|
|
132
|
+
return from === null
|
|
133
|
+
? `SELECT * FROM (${select} ORDER BY m.seq DESC LIMIT ${exports.OPENCODE_BACKFILL_ROWS}) ORDER BY seq`
|
|
134
|
+
: `${select} AND m.seq >= ${from} ORDER BY m.seq LIMIT ${exports.OPENCODE_ROWS_PER_READ}`;
|
|
135
|
+
};
|
|
136
|
+
exports.opencodeRowsSql = opencodeRowsSql;
|
|
137
|
+
const isoOf = (ms) => {
|
|
138
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms))
|
|
139
|
+
return {};
|
|
140
|
+
const date = new Date(ms);
|
|
141
|
+
return Number.isNaN(date.getTime()) ? {} : { at: date.toISOString() };
|
|
142
|
+
};
|
|
143
|
+
/** The row's `content`, or null when the query handed back something that is not a list. */
|
|
144
|
+
const itemsOf = (row) => {
|
|
145
|
+
if (row.items === undefined || row.items === null)
|
|
146
|
+
return [];
|
|
147
|
+
let parsed = row.items;
|
|
148
|
+
if (typeof parsed === 'string') {
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(parsed);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Each item is checked field by field where it is drawn.
|
|
157
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
158
|
+
};
|
|
159
|
+
const num = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : 0);
|
|
160
|
+
/** A line the projector reads as "this kind of thing is not drawn". */
|
|
161
|
+
const unknownLine = (what) => JSON.stringify({ unknown: what });
|
|
162
|
+
/**
|
|
163
|
+
* What these rows add to what was already sent, and where the next read starts.
|
|
164
|
+
*
|
|
165
|
+
* Every piece of a row gets a key (`<seq>:<part><index>`), and a key sent once
|
|
166
|
+
* is never sent again. A user row is final when written. An assistant row is
|
|
167
|
+
* not: its text is sent once something follows it in the row or the row is
|
|
168
|
+
* closed, a tool call once it runs, its result once the call has one, and
|
|
169
|
+
* the step's token count and any error when the step is finished. A row is
|
|
170
|
+
* closed when it is finished, when a later assistant step exists (opencode
|
|
171
|
+
* starts one only after the last is over, so an unfinished one behind it died
|
|
172
|
+
* mid-stream), or when it falls out of the `OPENCODE_OPEN_WINDOW` rows behind
|
|
173
|
+
* the newest — closing is what sends a dead step's last words, which nothing
|
|
174
|
+
* would otherwise send. `from` stays on the oldest row still open, so the next
|
|
175
|
+
* read sees it again. `emitted` keeps only the keys at or after `from`;
|
|
176
|
+
* nothing before it is read again.
|
|
177
|
+
*/
|
|
178
|
+
const opencodeLinesOf = (rows, sent) => {
|
|
179
|
+
const seen = new Set(sent.emitted);
|
|
180
|
+
const lines = [];
|
|
181
|
+
const out = (key, event) => {
|
|
182
|
+
if (seen.has(key))
|
|
183
|
+
return;
|
|
184
|
+
seen.add(key);
|
|
185
|
+
lines.push(JSON.stringify(event));
|
|
186
|
+
};
|
|
187
|
+
const once = (key, what) => {
|
|
188
|
+
if (seen.has(key))
|
|
189
|
+
return;
|
|
190
|
+
seen.add(key);
|
|
191
|
+
lines.push(unknownLine(what));
|
|
192
|
+
};
|
|
193
|
+
let newest = sent.from - 1;
|
|
194
|
+
let lastStep = -1;
|
|
195
|
+
for (const row of rows) {
|
|
196
|
+
newest = Math.max(newest, row.seq);
|
|
197
|
+
if (row.type === 'assistant')
|
|
198
|
+
lastStep = Math.max(lastStep, row.seq);
|
|
199
|
+
}
|
|
200
|
+
let oldestOpen = null;
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
const stamp = isoOf(row.created);
|
|
203
|
+
if (row.type === 'user') {
|
|
204
|
+
// A prompt of attachments alone has no text, and still starts a turn.
|
|
205
|
+
const text = (typeof row.text === 'string' ? row.text : '').trim() || '(attachment)';
|
|
206
|
+
out(`${row.seq}:p`, { kind: 'prompt', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (row.type !== 'assistant')
|
|
210
|
+
continue;
|
|
211
|
+
const done = typeof row.completed === 'number';
|
|
212
|
+
const closed = done || row.seq < lastStep || row.seq < newest - exports.OPENCODE_OPEN_WINDOW;
|
|
213
|
+
if (!closed && oldestOpen === null)
|
|
214
|
+
oldestOpen = row.seq;
|
|
215
|
+
const items = itemsOf(row);
|
|
216
|
+
if (items === null)
|
|
217
|
+
once(`${row.seq}:x`, 'opencode content that is not a list');
|
|
218
|
+
let thinking = 0;
|
|
219
|
+
(items ?? []).forEach((item, i) => {
|
|
220
|
+
if (!item || typeof item !== 'object') {
|
|
221
|
+
once(`${row.seq}:x${i}`, 'opencode content item that is not an object');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const at = item.at !== undefined && item.at !== null ? isoOf(item.at) : stamp;
|
|
225
|
+
if (item.type === 'reasoning') {
|
|
226
|
+
thinking += 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (item.type === 'text') {
|
|
230
|
+
const text = (item.text ?? '').trim();
|
|
231
|
+
const final = closed || i < (items ?? []).length - 1;
|
|
232
|
+
if (text && final)
|
|
233
|
+
out(`${row.seq}:t${i}`, { kind: 'text', text: (0, transcript_tail_js_1.clamp)(text, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...at });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (item.type === 'tool') {
|
|
237
|
+
if (typeof item.id !== 'string' || typeof item.name !== 'string') {
|
|
238
|
+
once(`${row.seq}:x${i}`, 'opencode tool call without an id or name');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// A call is first written while its input still streams in
|
|
242
|
+
// (measured on 2.0.24: a write seen then had no path yet), and a
|
|
243
|
+
// call sent once is never sent again — so wait until it runs.
|
|
244
|
+
const settled = item.status === 'running' || item.status === 'completed' || item.status === 'error';
|
|
245
|
+
if (!settled && !closed)
|
|
246
|
+
return;
|
|
247
|
+
const line = (0, transcript_tail_js_1.oneLine)(typeof item.target === 'string' ? item.target : undefined);
|
|
248
|
+
const target = line ? (0, transcript_tail_js_1.clamp)(line) : undefined;
|
|
249
|
+
out(`${row.seq}:u${i}`, { kind: 'tool', id: (0, transcript_tail_js_1.clamp)(item.id), name: (0, transcript_tail_js_1.clamp)(item.name), ...(target ? { target } : {}), ...at });
|
|
250
|
+
if (item.status === 'completed' || item.status === 'error') {
|
|
251
|
+
const count = num(item.lines);
|
|
252
|
+
out(`${row.seq}:r${i}`, { kind: 'tool_result', id: (0, transcript_tail_js_1.clamp)(item.id), ok: item.status === 'completed', ...(count ? { lines: count } : {}), ...at });
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
once(`${row.seq}:x${i}`, `opencode content type ${typeof item.type === 'string' ? item.type : '(none)'}`);
|
|
257
|
+
});
|
|
258
|
+
if (done && typeof row.errorType === 'string') {
|
|
259
|
+
// A failed step is finished like any other (measured: `time.completed`
|
|
260
|
+
// set beside `finish: "error"`), and its error is all it has to say.
|
|
261
|
+
const why = (0, transcript_tail_js_1.oneLine)(typeof row.errorText === 'string' ? row.errorText : undefined);
|
|
262
|
+
out(`${row.seq}:e`, { kind: 'text', text: (0, transcript_tail_js_1.clamp)(`opencode stopped: ${row.errorType}${why ? ` — ${why}` : ''}`, transcript_tail_js_1.MAX_EVENT_TEXT_CHARS), ...stamp });
|
|
263
|
+
}
|
|
264
|
+
if (done) {
|
|
265
|
+
out(`${row.seq}:m`, {
|
|
266
|
+
kind: 'msg',
|
|
267
|
+
mid: (0, transcript_tail_js_1.clamp)(row.id),
|
|
268
|
+
...(typeof row.model === 'string' ? { model: (0, transcript_tail_js_1.clamp)(row.model) } : {}),
|
|
269
|
+
out: num(row.out),
|
|
270
|
+
// `input` excludes the cached prompt (measured: input 279 beside
|
|
271
|
+
// cache.read 140,145), so the context is the three together.
|
|
272
|
+
ctx: num(row.input) + num(row.cacheRead) + num(row.cacheWrite),
|
|
273
|
+
...(thinking ? { thinking } : {}),
|
|
274
|
+
...stamp,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let from = oldestOpen ?? newest + 1;
|
|
279
|
+
from = Math.max(from, newest - exports.OPENCODE_OPEN_WINDOW, sent.from);
|
|
280
|
+
// A full page of finished rows behind one open row would be read again
|
|
281
|
+
// forever; past the page is where the next read has to start.
|
|
282
|
+
if (rows.length >= exports.OPENCODE_ROWS_PER_READ && from <= sent.from)
|
|
283
|
+
from = newest + 1;
|
|
284
|
+
const emitted = [...seen].filter((key) => Number(key.slice(0, key.indexOf(':'))) >= from);
|
|
285
|
+
return { lines, from, emitted };
|
|
286
|
+
};
|
|
287
|
+
exports.opencodeLinesOf = opencodeLinesOf;
|
|
288
|
+
const cursorOf = (prev, session) => {
|
|
289
|
+
const c = prev?.cursor;
|
|
290
|
+
if (!c || typeof c !== 'object')
|
|
291
|
+
return null;
|
|
292
|
+
const { session: s, from, emitted, stamp, queriedAt } = c;
|
|
293
|
+
if (s !== session || typeof from !== 'number' || !Number.isSafeInteger(from))
|
|
294
|
+
return null;
|
|
295
|
+
if (!Array.isArray(emitted) || typeof stamp !== 'string' || typeof queriedAt !== 'number')
|
|
296
|
+
return null;
|
|
297
|
+
return { session, from, emitted: emitted.filter((key) => typeof key === 'string'), stamp, queriedAt };
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* When a query last failed, per source path. A failure leaves no state to
|
|
301
|
+
* carry the time on (the reader returns null), and without it a store that
|
|
302
|
+
* cannot be read would be asked again on every 500 ms tick.
|
|
303
|
+
*/
|
|
304
|
+
const failedAt = new Map();
|
|
305
|
+
const fileStamp = (db) => {
|
|
306
|
+
const parts = [];
|
|
307
|
+
for (const path of [db, `${db}-wal`]) {
|
|
308
|
+
try {
|
|
309
|
+
const s = (0, fs_1.statSync)(path);
|
|
310
|
+
parts.push(`${s.size}:${s.mtimeMs}`);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// No WAL file is a normal state (checkpointed, or opencode not running).
|
|
314
|
+
parts.push('-');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return parts[0] === '-' ? null : parts.join('|');
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* What changed in one opencode session since `prev`, or null when nothing did
|
|
321
|
+
* — the store's files have not moved, the last query was under
|
|
322
|
+
* `OPENCODE_MIN_QUERY_MS` ago, or the store cannot be read. The WAL moves on
|
|
323
|
+
* every write by every opencode session on this machine, so the time floor is
|
|
324
|
+
* what keeps a busy neighbour from turning a 500 ms poll into a `sqlite3`
|
|
325
|
+
* spawn per tick.
|
|
326
|
+
*/
|
|
327
|
+
const readOpencodeTranscript = (path, prev, deps) => {
|
|
328
|
+
const source = parseSourcePath(path);
|
|
329
|
+
if (!source)
|
|
330
|
+
return null;
|
|
331
|
+
const now = deps.now ?? Date.now;
|
|
332
|
+
const stamp = (deps.stamp ?? fileStamp)(source.db);
|
|
333
|
+
if (stamp === null)
|
|
334
|
+
return null;
|
|
335
|
+
const cursor = cursorOf(prev, source.sessionId);
|
|
336
|
+
if (cursor && cursor.stamp === stamp)
|
|
337
|
+
return null;
|
|
338
|
+
const last = Math.max(cursor?.queriedAt ?? -Infinity, failedAt.get(path) ?? -Infinity);
|
|
339
|
+
if (now() - last < exports.OPENCODE_MIN_QUERY_MS)
|
|
340
|
+
return null;
|
|
341
|
+
const rows = deps.query(source.db, (0, exports.opencodeRowsSql)(source.sessionId, cursor ? cursor.from : null));
|
|
342
|
+
if (rows === null) {
|
|
343
|
+
failedAt.set(path, now());
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
failedAt.delete(path);
|
|
347
|
+
const next = (0, exports.opencodeLinesOf)(rows.filter(isRow), cursor ?? { from: 0, emitted: [] });
|
|
348
|
+
const state = {
|
|
349
|
+
offset: next.from,
|
|
350
|
+
// `agent read` says there is more above when the read was a window:
|
|
351
|
+
// a backfill that came back full stands for a session longer than it.
|
|
352
|
+
size: !cursor && rows.length >= exports.OPENCODE_BACKFILL_ROWS ? transcript_tail_js_1.TRANSCRIPT_BACKFILL_BYTES + 1 : 0,
|
|
353
|
+
mtimeMs: 0,
|
|
354
|
+
ino: 0,
|
|
355
|
+
carry: '',
|
|
356
|
+
resyncing: false,
|
|
357
|
+
salvage: [],
|
|
358
|
+
cursor: { session: source.sessionId, from: next.from, emitted: next.emitted, stamp, queriedAt: now() },
|
|
359
|
+
};
|
|
360
|
+
return { lines: next.lines, state, bytesRead: 0, droppedLines: 0 };
|
|
361
|
+
};
|
|
362
|
+
exports.readOpencodeTranscript = readOpencodeTranscript;
|
|
363
|
+
const EVENT_KINDS = new Set(['prompt', 'text', 'tool', 'tool_result', 'msg']);
|
|
364
|
+
/**
|
|
365
|
+
* The reader's lines back into events. They were built as events, so this
|
|
366
|
+
* only refuses what is not one and names what the reader marked unknown.
|
|
367
|
+
*/
|
|
368
|
+
const projectOpencodeEntries = (lines, opts = {}) => {
|
|
369
|
+
const events = [];
|
|
370
|
+
for (const raw of lines) {
|
|
371
|
+
let entry;
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(raw);
|
|
374
|
+
if (!parsed || typeof parsed !== 'object')
|
|
375
|
+
continue;
|
|
376
|
+
entry = parsed;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (typeof entry.unknown === 'string') {
|
|
382
|
+
opts.onUnknown?.(entry.unknown);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (typeof entry.kind === 'string' && EVENT_KINDS.has(entry.kind))
|
|
386
|
+
events.push(entry);
|
|
387
|
+
}
|
|
388
|
+
return (0, transcript_tail_js_1.sliceSinceLastPrompt)(events, opts);
|
|
389
|
+
};
|
|
390
|
+
exports.projectOpencodeEntries = projectOpencodeEntries;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HwipConfig } from './config.js';
|
|
2
|
+
export declare const optionalFeatures: (args: Record<string, string | boolean>, config: Pick<HwipConfig, "turnHistory" | "keepAwake">) => {
|
|
3
|
+
turnHistory: boolean;
|
|
4
|
+
keepAwake: boolean;
|
|
5
|
+
};
|
|
6
|
+
//# sourceMappingURL=optional-features.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.optionalFeatures = void 0;
|
|
4
|
+
const args_js_1 = require("./args.js");
|
|
5
|
+
/**
|
|
6
|
+
* The listener's opt-out features: on unless a flag or the config says no. Only an absent value or
|
|
7
|
+
* `true` keeps one on in the config: `"turnHistory": "false"`, `0` or `null` is someone's attempt to
|
|
8
|
+
* turn it off, and reading it as "not set" uploaded the conversations anyway. `loadConfig` reads a
|
|
9
|
+
* config.json it cannot use as every option off (`unreadableConfigDefaults`), so what this
|
|
10
|
+
* returns is the privacy-safe set then, with the flags unable to turn anything back on. A flag is
|
|
11
|
+
* read by `flagOn`: `--no-turn-history=true` and `=1` are as much a no as the bare form, and a
|
|
12
|
+
* value that is neither yes nor no throws `FlagValueError`.
|
|
13
|
+
*/
|
|
14
|
+
const configKeepsOn = (value) => value === undefined || value === true;
|
|
15
|
+
const optionalFeatures = (args, config) => ({
|
|
16
|
+
turnHistory: !(0, args_js_1.flagOn)(args, 'no-turn-history') && configKeepsOn(config.turnHistory),
|
|
17
|
+
keepAwake: !(0, args_js_1.flagOn)(args, 'no-keep-awake') && configKeepsOn(config.keepAwake),
|
|
18
|
+
});
|
|
19
|
+
exports.optionalFeatures = optionalFeatures;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fit a detached tmux window to the phone that is watching it.
|
|
3
|
+
*
|
|
4
|
+
* A pane is as wide as the terminal that last drew it, usually a desktop's
|
|
5
|
+
* 120-odd columns. The phone mirror shows about 45, so every full-width row a
|
|
6
|
+
* prompt or TUI draws (a rule, a right-aligned clock, a status line) folds into
|
|
7
|
+
* two. Nobody else sees a DETACHED window, so while a phone watches one it is
|
|
8
|
+
* resized to the phone's width and the program redraws to fit; when the phone
|
|
9
|
+
* leaves, the old width comes back.
|
|
10
|
+
*
|
|
11
|
+
* An attached window is left alone: shrinking it would shrink the desktop
|
|
12
|
+
* terminal of whoever is attached. A window split into panes is left alone
|
|
13
|
+
* too, since the window width is not any one pane's width. And when a client
|
|
14
|
+
* attaches while the phone is watching, the next check hands the window back
|
|
15
|
+
* to tmux, which sizes it to that client.
|
|
16
|
+
*
|
|
17
|
+
* A fitted window carries its old width in `@hwip_fit_width`. A listener that
|
|
18
|
+
* died without releasing (a crash, a SIGKILL) leaves that mark behind, and
|
|
19
|
+
* the next one to start gives those windows back ({@link releaseStaleFits}).
|
|
20
|
+
*/
|
|
21
|
+
/** Runs one tmux command (socket-aware) and returns what it printed. */
|
|
22
|
+
export type TmuxRun = (args: string[]) => {
|
|
23
|
+
status: number | null;
|
|
24
|
+
stdout: string;
|
|
25
|
+
};
|
|
26
|
+
export declare const PANE_FIT_MIN_COLS = 20;
|
|
27
|
+
export declare const PANE_FIT_MAX_COLS = 400;
|
|
28
|
+
/** A viewer's column count, or null when it did not send a usable one. */
|
|
29
|
+
export declare const clampViewerCols: (cols: unknown) => number | null;
|
|
30
|
+
/**
|
|
31
|
+
* Hand back every window a previous listener fitted and never released:
|
|
32
|
+
* restore its width when detached, then drop the manual size and the mark.
|
|
33
|
+
*/
|
|
34
|
+
export declare const releaseStaleFits: (run: TmuxRun) => number;
|
|
35
|
+
export declare const createPaneFitter: (run: TmuxRun) => {
|
|
36
|
+
fit: (session: string, target: string, cols: unknown) => void;
|
|
37
|
+
release: (session: string) => void;
|
|
38
|
+
fitted: (session: string) => boolean;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=pane-fit.d.ts.map
|
package/dist/pane-fit.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fit a detached tmux window to the phone that is watching it.
|
|
4
|
+
*
|
|
5
|
+
* A pane is as wide as the terminal that last drew it, usually a desktop's
|
|
6
|
+
* 120-odd columns. The phone mirror shows about 45, so every full-width row a
|
|
7
|
+
* prompt or TUI draws (a rule, a right-aligned clock, a status line) folds into
|
|
8
|
+
* two. Nobody else sees a DETACHED window, so while a phone watches one it is
|
|
9
|
+
* resized to the phone's width and the program redraws to fit; when the phone
|
|
10
|
+
* leaves, the old width comes back.
|
|
11
|
+
*
|
|
12
|
+
* An attached window is left alone: shrinking it would shrink the desktop
|
|
13
|
+
* terminal of whoever is attached. A window split into panes is left alone
|
|
14
|
+
* too, since the window width is not any one pane's width. And when a client
|
|
15
|
+
* attaches while the phone is watching, the next check hands the window back
|
|
16
|
+
* to tmux, which sizes it to that client.
|
|
17
|
+
*
|
|
18
|
+
* A fitted window carries its old width in `@hwip_fit_width`. A listener that
|
|
19
|
+
* died without releasing (a crash, a SIGKILL) leaves that mark behind, and
|
|
20
|
+
* the next one to start gives those windows back ({@link releaseStaleFits}).
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.createPaneFitter = exports.releaseStaleFits = exports.clampViewerCols = exports.PANE_FIT_MAX_COLS = exports.PANE_FIT_MIN_COLS = void 0;
|
|
24
|
+
exports.PANE_FIT_MIN_COLS = 20;
|
|
25
|
+
exports.PANE_FIT_MAX_COLS = 400;
|
|
26
|
+
/** A viewer's column count, or null when it did not send a usable one. */
|
|
27
|
+
const clampViewerCols = (cols) => {
|
|
28
|
+
if (typeof cols !== 'number' || !Number.isFinite(cols))
|
|
29
|
+
return null;
|
|
30
|
+
return Math.min(exports.PANE_FIT_MAX_COLS, Math.max(exports.PANE_FIT_MIN_COLS, Math.floor(cols)));
|
|
31
|
+
};
|
|
32
|
+
exports.clampViewerCols = clampViewerCols;
|
|
33
|
+
const SEP = '\u241f';
|
|
34
|
+
const FIT_MARK = '@hwip_fit_width';
|
|
35
|
+
/**
|
|
36
|
+
* Hand back every window a previous listener fitted and never released:
|
|
37
|
+
* restore its width when detached, then drop the manual size and the mark.
|
|
38
|
+
*/
|
|
39
|
+
const releaseStaleFits = (run) => {
|
|
40
|
+
const r = run(['list-windows', '-a', '-F', `#{window_id}${SEP}#{${FIT_MARK}}${SEP}#{session_attached}`]);
|
|
41
|
+
if (r.status !== 0)
|
|
42
|
+
return 0;
|
|
43
|
+
let released = 0;
|
|
44
|
+
for (const line of r.stdout.split('\n')) {
|
|
45
|
+
const [id, mark, attached] = line.split(SEP);
|
|
46
|
+
const width = Number(mark);
|
|
47
|
+
if (!id || !mark || !Number.isFinite(width))
|
|
48
|
+
continue;
|
|
49
|
+
if (attached === '0')
|
|
50
|
+
run(['resize-window', '-t', id, '-x', String(width)]);
|
|
51
|
+
run(['set-option', '-w', '-u', '-t', id, 'window-size']);
|
|
52
|
+
run(['set-option', '-w', '-u', '-t', id, FIT_MARK]);
|
|
53
|
+
released++;
|
|
54
|
+
}
|
|
55
|
+
return released;
|
|
56
|
+
};
|
|
57
|
+
exports.releaseStaleFits = releaseStaleFits;
|
|
58
|
+
const readWindow = (run, target) => {
|
|
59
|
+
const r = run(['display-message', '-p', '-t', target,
|
|
60
|
+
`#{session_attached}${SEP}#{window_panes}${SEP}#{window_width}`]);
|
|
61
|
+
if (r.status !== 0)
|
|
62
|
+
return null;
|
|
63
|
+
const [attached, panes, width] = r.stdout.trim().split(SEP).map(Number);
|
|
64
|
+
if (![attached, panes, width].every(Number.isFinite))
|
|
65
|
+
return null;
|
|
66
|
+
return { attached, panes, width };
|
|
67
|
+
};
|
|
68
|
+
const createPaneFitter = (run) => {
|
|
69
|
+
const fits = new Map();
|
|
70
|
+
/**
|
|
71
|
+
* Give the window back: restore its width if nobody is attached, then drop
|
|
72
|
+
* the manual size `resize-window` set, so the next client to attach sizes
|
|
73
|
+
* it again. With a client attached, dropping the option alone resizes it to
|
|
74
|
+
* that client.
|
|
75
|
+
*/
|
|
76
|
+
const release = (session) => {
|
|
77
|
+
const fit = fits.get(session);
|
|
78
|
+
if (!fit)
|
|
79
|
+
return;
|
|
80
|
+
fits.delete(session);
|
|
81
|
+
const win = readWindow(run, fit.target);
|
|
82
|
+
if (win && win.attached === 0)
|
|
83
|
+
run(['resize-window', '-t', fit.target, '-x', String(fit.width)]);
|
|
84
|
+
run(['set-option', '-w', '-u', '-t', fit.target, 'window-size']);
|
|
85
|
+
run(['set-option', '-w', '-u', '-t', fit.target, FIT_MARK]);
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Size `session`'s window to `cols` if it is detached and unsplit. Safe to
|
|
89
|
+
* call on every renew: an unchanged width costs one display-message, and a
|
|
90
|
+
* window that gained a client since is released.
|
|
91
|
+
*/
|
|
92
|
+
const fit = (session, target, cols) => {
|
|
93
|
+
const want = (0, exports.clampViewerCols)(cols);
|
|
94
|
+
if (want === null)
|
|
95
|
+
return;
|
|
96
|
+
const win = readWindow(run, target);
|
|
97
|
+
if (!win)
|
|
98
|
+
return;
|
|
99
|
+
const held = fits.get(session);
|
|
100
|
+
if (win.attached > 0 || win.panes !== 1) {
|
|
101
|
+
if (held)
|
|
102
|
+
release(session);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (held && held.cols === want && win.width === want)
|
|
106
|
+
return;
|
|
107
|
+
const width = held?.width ?? win.width;
|
|
108
|
+
// Mark first: a crash between the two must still be recoverable.
|
|
109
|
+
run(['set-option', '-w', '-t', target, FIT_MARK, String(width)]);
|
|
110
|
+
if (run(['resize-window', '-t', target, '-x', String(want)]).status !== 0) {
|
|
111
|
+
if (!held)
|
|
112
|
+
run(['set-option', '-w', '-u', '-t', target, FIT_MARK]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
fits.set(session, { target, width, cols: want });
|
|
116
|
+
};
|
|
117
|
+
return { fit, release, fitted: (session) => fits.has(session) };
|
|
118
|
+
};
|
|
119
|
+
exports.createPaneFitter = createPaneFitter;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which key to believe for a device that signs something with its own key.
|
|
3
|
+
*
|
|
4
|
+
* An envelope that opens only shows its sender held *a* key (push v1 carries
|
|
5
|
+
* the sender's public key unsigned), so the reader must say whose key it
|
|
6
|
+
* expects. This answers that for one device id: the key the account's device
|
|
7
|
+
* list registers for it. The device is pinned on the way (`observeDevices`) — a first
|
|
8
|
+
* sight pins it, a key that changed replaces the pin with one warning and a record
|
|
9
|
+
* (`key-changes.ts`). The pin is a record for a person (`hwip trust`) and for the
|
|
10
|
+
* listener; it does not decide here, the list does.
|
|
11
|
+
*/
|
|
12
|
+
export type PeerKeyRefusal =
|
|
13
|
+
/** Not a device id, or not in the account's device list. */
|
|
14
|
+
'unlisted'
|
|
15
|
+
/** The device list could not be fetched. */
|
|
16
|
+
| 'devices_unavailable';
|
|
17
|
+
export type PeerKey = {
|
|
18
|
+
key: string;
|
|
19
|
+
} | {
|
|
20
|
+
refused: PeerKeyRefusal;
|
|
21
|
+
};
|
|
22
|
+
export interface PeerKeyDeps {
|
|
23
|
+
/** The key the account's device list registers for `deviceId`: null when none is listed, a rejection when the list cannot be had. */
|
|
24
|
+
registeredKey: (deviceId: string) => Promise<string | null>;
|
|
25
|
+
/** The listener's state file (`~/.hwip/trusted-devices.json`). */
|
|
26
|
+
trustPath: string;
|
|
27
|
+
/** This computer's own id: it never pins itself. */
|
|
28
|
+
selfId?: string;
|
|
29
|
+
/** One line for a person, as `observeDevices` words it. */
|
|
30
|
+
warn?: (line: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare const peerKeyOf: (deviceId: string, deps: PeerKeyDeps) => Promise<PeerKey>;
|
|
33
|
+
//# sourceMappingURL=peer-key.d.ts.map
|
package/dist/peer-key.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Which key to believe for a device that signs something with its own key.
|
|
4
|
+
*
|
|
5
|
+
* An envelope that opens only shows its sender held *a* key (push v1 carries
|
|
6
|
+
* the sender's public key unsigned), so the reader must say whose key it
|
|
7
|
+
* expects. This answers that for one device id: the key the account's device
|
|
8
|
+
* list registers for it. The device is pinned on the way (`observeDevices`) — a first
|
|
9
|
+
* sight pins it, a key that changed replaces the pin with one warning and a record
|
|
10
|
+
* (`key-changes.ts`). The pin is a record for a person (`hwip trust`) and for the
|
|
11
|
+
* listener; it does not decide here, the list does.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.peerKeyOf = void 0;
|
|
15
|
+
const crypto_js_1 = require("./crypto.js");
|
|
16
|
+
const subscriber_trust_js_1 = require("./subscriber-trust.js");
|
|
17
|
+
const trust_pins_js_1 = require("./trust-pins.js");
|
|
18
|
+
const peerKeyOf = async (deviceId, deps) => {
|
|
19
|
+
if (!subscriber_trust_js_1.DEVICE_ID.test(deviceId))
|
|
20
|
+
return { refused: 'unlisted' };
|
|
21
|
+
let registered;
|
|
22
|
+
try {
|
|
23
|
+
registered = await deps.registeredKey(deviceId);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { refused: 'devices_unavailable' };
|
|
27
|
+
}
|
|
28
|
+
// A listed key that is no device key in the one spelling is as good as none.
|
|
29
|
+
if (!registered || !(0, crypto_js_1.samePublicKey)(registered, registered))
|
|
30
|
+
return { refused: 'unlisted' };
|
|
31
|
+
(0, trust_pins_js_1.observeDevices)(deps.trustPath, [{ deviceId, publicKey: registered }], { selfId: deps.selfId, warn: deps.warn ?? (() => undefined) });
|
|
32
|
+
return { key: registered };
|
|
33
|
+
};
|
|
34
|
+
exports.peerKeyOf = peerKeyOf;
|