@pinchpass/cli 1.1.0 → 1.2.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/README.md +28 -4
- package/dist/args.js +30 -0
- package/dist/binary.js +124 -0
- package/dist/env.js +62 -0
- package/dist/index.js +317 -269
- package/dist/json-stream.js +48 -0
- package/package.json +24 -12
- package/scripts/install-binary.js +189 -75
- package/skills/pinchpass/SKILL.md +23 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental parser for the CLI's `-json` output.
|
|
3
|
+
*
|
|
4
|
+
* The CLI prints two pretty-printed JSON objects (before and after waiting),
|
|
5
|
+
* so a byte stream can split them across chunks. `takeJsonBlobs` returns every
|
|
6
|
+
* complete top-level object and leaves any partial trailing object in `rest`.
|
|
7
|
+
*/
|
|
8
|
+
export function takeJsonBlobs(text) {
|
|
9
|
+
const blobs = [];
|
|
10
|
+
let depth = 0;
|
|
11
|
+
let start = -1;
|
|
12
|
+
let inString = false;
|
|
13
|
+
let escaped = false;
|
|
14
|
+
for (let i = 0; i < text.length; i++) {
|
|
15
|
+
const c = text[i];
|
|
16
|
+
if (inString) {
|
|
17
|
+
if (escaped)
|
|
18
|
+
escaped = false;
|
|
19
|
+
else if (c === "\\")
|
|
20
|
+
escaped = true;
|
|
21
|
+
else if (c === '"')
|
|
22
|
+
inString = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (c === '"') {
|
|
26
|
+
inString = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (c === "{") {
|
|
30
|
+
if (depth === 0)
|
|
31
|
+
start = i;
|
|
32
|
+
depth++;
|
|
33
|
+
}
|
|
34
|
+
else if (c === "}") {
|
|
35
|
+
depth = Math.max(0, depth - 1);
|
|
36
|
+
if (depth === 0 && start >= 0) {
|
|
37
|
+
try {
|
|
38
|
+
blobs.push(JSON.parse(text.slice(start, i + 1)));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// skip malformed output
|
|
42
|
+
}
|
|
43
|
+
start = -1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { blobs, rest: start >= 0 ? text.slice(start) : "" };
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinchpass/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "One-time E2E-encrypted secret request links — native for Pi and OpenClaw agents",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,32 +24,44 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"pi": {
|
|
27
|
-
"extensions": [
|
|
28
|
-
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./dist/index.js"
|
|
29
|
+
],
|
|
30
|
+
"skills": [
|
|
31
|
+
"./skills"
|
|
32
|
+
],
|
|
33
|
+
"image": "https://raw.githubusercontent.com/rubybear-lgtm/PinchPass/master/pinchpass.png"
|
|
29
34
|
},
|
|
30
35
|
"openclaw": {
|
|
31
|
-
"extensions": [
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./dist/index.js"
|
|
38
|
+
]
|
|
32
39
|
},
|
|
33
40
|
"scripts": {
|
|
34
41
|
"build": "tsc && node scripts/copy-skill.js",
|
|
42
|
+
"test": "npm run build && node --test \"test/**/*.test.js\"",
|
|
35
43
|
"postinstall": "node scripts/install-binary.js",
|
|
36
44
|
"prepublishOnly": "npm run build"
|
|
37
45
|
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"@opencode-ai/plugin": "^1.16.2"
|
|
40
|
-
},
|
|
41
46
|
"peerDependencies": {
|
|
42
47
|
"@earendil-works/pi-ai": "*",
|
|
43
48
|
"@earendil-works/pi-coding-agent": "*",
|
|
44
49
|
"@earendil-works/pi-tui": "*",
|
|
50
|
+
"@opencode-ai/plugin": "^1.16.2",
|
|
45
51
|
"typebox": "*"
|
|
46
52
|
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@opencode-ai/plugin": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
47
58
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-ai": "
|
|
49
|
-
"@earendil-works/pi-coding-agent": "^
|
|
50
|
-
"@earendil-works/pi-tui": "
|
|
51
|
-
"
|
|
52
|
-
"
|
|
59
|
+
"@earendil-works/pi-ai": "^1.2.0",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "^1.2.0",
|
|
61
|
+
"@earendil-works/pi-tui": "^1.2.0",
|
|
62
|
+
"@opencode-ai/plugin": "^1.19.0",
|
|
63
|
+
"typebox": "^1.3.37",
|
|
64
|
+
"typescript": "^7.0.2"
|
|
53
65
|
},
|
|
54
66
|
"publishConfig": {
|
|
55
67
|
"access": "public"
|
|
@@ -1,27 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Install or update the pinchpass binary for the current platform.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 2. If it already exists in a known install dir, nothing to do.
|
|
8
|
-
* 3. Install to ~/.local/bin (universal user bin — the PinchPass Pi
|
|
9
|
-
* extension and OpenClaw plugin both find it there without PATH changes).
|
|
5
|
+
* npm runs this as a `postinstall`, so it must be idempotent and it must make
|
|
6
|
+
* `npm update -g @pinchpass/cli` actually upgrade the binary:
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* 1. Look for an existing `pinchpass` (PATH, then the known install dirs).
|
|
9
|
+
* 2. If one already reports the package's version, do nothing.
|
|
10
|
+
* 3. Otherwise download the release asset pinned to the package version,
|
|
11
|
+
* verify it against the release's checksums.txt, and install it to
|
|
12
|
+
* ~/.local/bin.
|
|
13
|
+
*
|
|
14
|
+
* Set PINCHPASS_SKIP_DOWNLOAD=1 to skip the download entirely (used by CI).
|
|
13
15
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import {
|
|
19
|
+
chmodSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
statSync,
|
|
25
|
+
unlinkSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
31
|
|
|
19
|
-
const VERSION = "latest";
|
|
20
32
|
const REPO = "rubybear-lgtm/PinchPass";
|
|
21
33
|
|
|
22
|
-
/** Real
|
|
34
|
+
/** Real binaries are multi-MB; a failed download leaves a tiny stub behind. */
|
|
23
35
|
const MIN_VALID_BYTES = 100 * 1024;
|
|
24
36
|
|
|
37
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
|
|
39
|
+
export function packageVersion() {
|
|
40
|
+
return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function binaryName(platform = process.platform) {
|
|
44
|
+
return platform === "win32" ? "pinchpass.exe" : "pinchpass";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assetName(platform = process.platform, arch = process.arch) {
|
|
48
|
+
const os = { darwin: "darwin", linux: "linux", win32: "windows" }[platform];
|
|
49
|
+
if (!os) throw new Error(`Unsupported platform: ${platform}`);
|
|
50
|
+
return `pinchpass-${os}-${arch === "arm64" ? "arm64" : "amd64"}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Extract an x.y.z version from `pinchpass --version` output. */
|
|
54
|
+
export function parseVersion(text) {
|
|
55
|
+
const match = /(?:^|\s)v?(\d+\.\d+\.\d+)(?=\s|$)/.exec(text ?? "");
|
|
56
|
+
return match ? match[1] : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Numeric x.y.z comparison. Returns -1, 0, or 1. */
|
|
60
|
+
export function compareVersions(a, b) {
|
|
61
|
+
const pa = a.split(".").map(Number);
|
|
62
|
+
const pb = b.split(".").map(Number);
|
|
63
|
+
for (let i = 0; i < 3; i++) {
|
|
64
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
65
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sha256(bytes) {
|
|
71
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Find the SHA-256 for `asset` in a checksums.txt body. */
|
|
75
|
+
export function checksumFor(text, asset) {
|
|
76
|
+
for (const line of (text ?? "").split("\n")) {
|
|
77
|
+
const [hash, name] = line.trim().split(/\s+/);
|
|
78
|
+
if (name === asset && /^[0-9a-f]{64}$/i.test(hash)) return hash.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function knownDirs(home = homedir()) {
|
|
84
|
+
return [join(home, ".local", "bin"), join(home, ".openclaw", "bin"), join(home, ".pi", "bin")];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function searchDirs() {
|
|
88
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
89
|
+
return [...(process.env.PATH ?? "").split(sep).filter(Boolean), ...knownDirs()];
|
|
90
|
+
}
|
|
91
|
+
|
|
25
92
|
function isValidBinary(path) {
|
|
26
93
|
try {
|
|
27
94
|
return statSync(path).size >= MIN_VALID_BYTES;
|
|
@@ -30,78 +97,125 @@ function isValidBinary(path) {
|
|
|
30
97
|
}
|
|
31
98
|
}
|
|
32
99
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const osMap = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
36
|
-
const plat = osMap[process.platform];
|
|
37
|
-
if (!plat) throw new Error(`Unsupported platform: ${process.platform}`);
|
|
38
|
-
return { name: `pinchpass-${plat}-${arch}`, plat, arch };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function onPath(name) {
|
|
100
|
+
/** Read the version a binary reports; null for pre-1.2.0 builds (no --version). */
|
|
101
|
+
export function readBinaryVersion(bin) {
|
|
42
102
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.split(/\r?\n/)[0];
|
|
50
|
-
return found || null;
|
|
103
|
+
const out = execFileSync(bin, ["--version"], {
|
|
104
|
+
encoding: "utf8",
|
|
105
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
106
|
+
timeout: 5000,
|
|
107
|
+
});
|
|
108
|
+
return parseVersion(out);
|
|
51
109
|
} catch {
|
|
52
110
|
return null;
|
|
53
111
|
}
|
|
54
112
|
}
|
|
55
113
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
join(homedir(), ".local", "bin"),
|
|
61
|
-
join(homedir(), ".openclaw", "bin"),
|
|
62
|
-
join(homedir(), ".pi", "bin"),
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
// 1. Already on PATH?
|
|
66
|
-
const onPathLocation = onPath(`pinchpass${exe}`);
|
|
67
|
-
if (onPathLocation && isValidBinary(onPathLocation)) {
|
|
68
|
-
console.log(`pinchpass already available on PATH at ${onPathLocation}`);
|
|
69
|
-
return;
|
|
114
|
+
export function findExisting(name, dirs = searchDirs()) {
|
|
115
|
+
for (const dir of dirs) {
|
|
116
|
+
const candidate = join(dir, name);
|
|
117
|
+
if (existsSync(candidate) && isValidBinary(candidate)) return candidate;
|
|
70
118
|
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
71
121
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
async function fetchBytes(url) {
|
|
123
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
124
|
+
if (!res.ok) return { ok: false, status: res.status };
|
|
125
|
+
return { ok: true, bytes: Buffer.from(await res.arrayBuffer()) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Download the asset for `version`, falling back to `latest` if it is missing. */
|
|
129
|
+
export async function downloadAsset(asset, version) {
|
|
130
|
+
const versioned = `https://github.com/${REPO}/releases/download/v${version}/${asset}`;
|
|
131
|
+
const first = await fetchBytes(versioned);
|
|
132
|
+
if (first.ok) {
|
|
133
|
+
return { bytes: first.bytes, base: versioned.slice(0, -asset.length - 1) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const latest = `https://github.com/${REPO}/releases/latest/download/${asset}`;
|
|
137
|
+
const second = await fetchBytes(latest);
|
|
138
|
+
if (second.ok) {
|
|
139
|
+
return { bytes: second.bytes, base: latest.slice(0, -asset.length - 1), fallback: true };
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`could not download ${asset} (v${version}: ${first.status}, latest: ${second.status})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Returns "verified", "missing", or throws on a checksum mismatch. */
|
|
145
|
+
export async function verifyChecksum(asset, bytes, base) {
|
|
146
|
+
const res = await fetch(`${base}/checksums.txt`, { redirect: "follow" });
|
|
147
|
+
if (!res.ok) return "missing";
|
|
148
|
+
const expected = checksumFor(await res.text(), asset);
|
|
149
|
+
if (!expected) return "missing";
|
|
150
|
+
if (expected !== sha256(bytes)) throw new Error(`checksum mismatch for ${asset}`);
|
|
151
|
+
return "verified";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function writeExecutable(target, bytes) {
|
|
155
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
156
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
157
|
+
writeFileSync(tmp, bytes, { mode: 0o755 });
|
|
158
|
+
if (process.platform !== "win32") chmodSync(tmp, 0o755);
|
|
159
|
+
try {
|
|
160
|
+
renameSync(tmp, target);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
try {
|
|
163
|
+
unlinkSync(tmp);
|
|
164
|
+
} catch {
|
|
165
|
+
// nothing to clean up
|
|
78
166
|
}
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function install({ log = console.log, warn = console.warn } = {}) {
|
|
172
|
+
if (process.env.PINCHPASS_SKIP_DOWNLOAD) {
|
|
173
|
+
log("pinchpass: PINCHPASS_SKIP_DOWNLOAD set; skipping binary download");
|
|
174
|
+
return { installed: false, reason: "skipped" };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const version = packageVersion();
|
|
178
|
+
const name = binaryName();
|
|
179
|
+
const existing = findExisting(name);
|
|
180
|
+
const existingVersion = existing ? readBinaryVersion(existing) : null;
|
|
181
|
+
|
|
182
|
+
if (existing && existingVersion === version) {
|
|
183
|
+
log(`pinchpass ${version} already installed at ${existing}`);
|
|
184
|
+
return { installed: false, reason: "up-to-date", path: existing };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const asset = assetName();
|
|
188
|
+
const { bytes, base, fallback } = await downloadAsset(asset, version);
|
|
189
|
+
if (fallback) {
|
|
190
|
+
warn(`pinchpass: no release asset for v${version}; installed the latest release instead`);
|
|
79
191
|
}
|
|
80
192
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const url =
|
|
85
|
-
VERSION === "latest"
|
|
86
|
-
? `https://github.com/${REPO}/releases/latest/download/${name}`
|
|
87
|
-
: `https://github.com/${REPO}/releases/download/v${VERSION}/${name}`;
|
|
88
|
-
|
|
89
|
-
console.log(`Downloading pinchpass from ${url}...`);
|
|
90
|
-
|
|
91
|
-
mkdirSync(installDir, { recursive: true });
|
|
92
|
-
execSync(`curl -sL "${url}" -o "${dest}"`, { stdio: "inherit" });
|
|
93
|
-
if (process.platform !== "win32") chmodSync(dest, 0o755);
|
|
94
|
-
|
|
95
|
-
console.log(`Installed pinchpass to ${dest}`);
|
|
96
|
-
const onPathNow = onPath(`pinchpass${exe}`);
|
|
97
|
-
if (!onPathNow) {
|
|
98
|
-
console.log(`Add ${installDir} to your PATH if you want to use the CLI directly.`);
|
|
99
|
-
console.log("(The PinchPass Pi extension and OpenClaw plugin find the binary without PATH changes.)");
|
|
193
|
+
const checksum = await verifyChecksum(asset, bytes, base);
|
|
194
|
+
if (checksum !== "verified") {
|
|
195
|
+
warn(`pinchpass: checksums.txt unavailable for ${asset}; installed without verification`);
|
|
100
196
|
}
|
|
197
|
+
|
|
198
|
+
const target = join(homedir(), ".local", "bin", name);
|
|
199
|
+
writeExecutable(target, bytes);
|
|
200
|
+
|
|
201
|
+
const from = existingVersion ? `v${existingVersion}` : "no previous binary";
|
|
202
|
+
log(`pinchpass: ${from} -> ${fallback ? "the latest release" : `v${version}`} at ${target}`);
|
|
203
|
+
|
|
204
|
+
if (existing && existing !== target && !knownDirs().some((dir) => existing.startsWith(dir))) {
|
|
205
|
+
warn(`pinchpass: ${existing} shadows the new binary on PATH — remove or update it`);
|
|
206
|
+
}
|
|
207
|
+
return { installed: true, version, path: target, checksum };
|
|
101
208
|
}
|
|
102
209
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
210
|
+
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
211
|
+
|
|
212
|
+
if (invokedDirectly) {
|
|
213
|
+
install().catch((err) => {
|
|
214
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
215
|
+
// Never fail the npm install: an existing binary or `go install` still works.
|
|
216
|
+
console.warn(`pinchpass: could not install the binary — ${message}`);
|
|
217
|
+
console.warn(
|
|
218
|
+
"pinchpass: install it later with `go install github.com/rubybear-lgtm/pinchpass@latest` or `npm i -g @pinchpass/cli`",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -26,6 +26,11 @@ which pinchpass
|
|
|
26
26
|
|
|
27
27
|
If missing, install it (pick one):
|
|
28
28
|
|
|
29
|
+
```bash
|
|
30
|
+
# Homebrew (macOS and Linux, via the tap)
|
|
31
|
+
brew install --cask rubybear-lgtm/tap/pinchpass
|
|
32
|
+
```
|
|
33
|
+
|
|
29
34
|
```bash
|
|
30
35
|
# Pi (native package — registers the request_secret tool and this skill)
|
|
31
36
|
pi install npm:@pinchpass/cli
|
|
@@ -37,7 +42,7 @@ npm i -g @pinchpass/cli
|
|
|
37
42
|
```
|
|
38
43
|
|
|
39
44
|
```bash
|
|
40
|
-
# From source (requires Go 1.
|
|
45
|
+
# From source (requires Go 1.26+)
|
|
41
46
|
go install github.com/rubybear-lgtm/pinchpass@latest
|
|
42
47
|
```
|
|
43
48
|
|
|
@@ -48,6 +53,10 @@ curl -sL https://github.com/rubybear-lgtm/PinchPass/releases/latest/download/pin
|
|
|
48
53
|
chmod +x /usr/local/bin/pinchpass
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+
Check the build with `pinchpass --version`, and update through the channel you
|
|
57
|
+
installed with — `brew upgrade --cask pinchpass`, `npm update -g @pinchpass/cli`,
|
|
58
|
+
or `go install github.com/rubybear-lgtm/pinchpass@latest`.
|
|
59
|
+
|
|
51
60
|
## Usage
|
|
52
61
|
|
|
53
62
|
Always use `-json` mode so you can parse the output programmatically.
|
|
@@ -99,14 +108,24 @@ two collection modes (`via` param):
|
|
|
99
108
|
- **`modal`** (default in the interactive TUI): a Pi dialog collects each
|
|
100
109
|
secret directly into the `.env` file. The value goes straight from the
|
|
101
110
|
user's keyboard to disk — never to the LLM, never into the transcript, no
|
|
102
|
-
server, no network. Cancelable with `Esc`.
|
|
111
|
+
server, no network. Cancelable with `Esc`. `ttl` does not apply to this mode
|
|
112
|
+
and is ignored.
|
|
103
113
|
- **`link`**: spawns the CLI, streams the one-time E2E-encrypted browser link
|
|
104
114
|
to the user immediately, blocks until submit/TTL. Use when the user is not
|
|
105
115
|
in the same Pi session (remote, tunneled, or RPC clients without a dialog
|
|
106
116
|
handler).
|
|
107
117
|
|
|
108
|
-
`via` defaults to `auto`: modal in the TUI, link elsewhere.
|
|
109
|
-
|
|
118
|
+
`via` defaults to `auto`: modal in the TUI, link elsewhere. Setting
|
|
119
|
+
`tunnel: true` always uses the link, even in the TUI, because the modal cannot
|
|
120
|
+
serve a remote user.
|
|
121
|
+
|
|
122
|
+
**Privacy note:** in `link` mode the full URL — including the `#k=` decryption
|
|
123
|
+
key fragment — is returned to the model and recorded in the transcript. Only
|
|
124
|
+
`modal` keeps the secret out of the transcript entirely. Prefer `modal` when
|
|
125
|
+
the user is local and the value is sensitive; use `link` when you must send a
|
|
126
|
+
URL over a channel the model can see.
|
|
127
|
+
|
|
128
|
+
Prefer the tool on Pi; use the CLI workflow below on other agents.
|
|
110
129
|
|
|
111
130
|
## Workflow
|
|
112
131
|
|