@tarfa/cli 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/README.md +3 -2
- package/dist/{chunk-KTUXNG4Q.js → chunk-4M2ZRGQR.js} +739 -338
- package/dist/cli.js +143 -69
- package/dist/serve.js +38 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CodexAppServer,
|
|
4
|
+
DICTATION_PROVIDERS,
|
|
4
5
|
FileClaudeSessionsStore,
|
|
5
6
|
FileDeviceStore,
|
|
7
|
+
FileDictationKeysStore,
|
|
6
8
|
FileHiddenFoldersStore,
|
|
7
9
|
FilePairingStore,
|
|
8
10
|
FileParkedStore,
|
|
@@ -17,9 +19,10 @@ import {
|
|
|
17
19
|
STATE_DIR,
|
|
18
20
|
ShortWorkspaceIds,
|
|
19
21
|
TmuxAdapter,
|
|
20
|
-
addPairing,
|
|
21
22
|
agentEnvFile,
|
|
23
|
+
atomicWrite,
|
|
22
24
|
browseDir,
|
|
25
|
+
changeDictationKey,
|
|
23
26
|
childEnv,
|
|
24
27
|
cloneOfUnit,
|
|
25
28
|
codexVersions,
|
|
@@ -37,7 +40,6 @@ import {
|
|
|
37
40
|
isEntrypoint,
|
|
38
41
|
isHiddenPath,
|
|
39
42
|
isMachineUrl,
|
|
40
|
-
isPairingPath,
|
|
41
43
|
listWorkspaces,
|
|
42
44
|
loadConfig,
|
|
43
45
|
loadOrCreateAgentKeys,
|
|
@@ -46,14 +48,18 @@ import {
|
|
|
46
48
|
loginShellResolver,
|
|
47
49
|
mainCheckout,
|
|
48
50
|
migrateStateDir,
|
|
51
|
+
mintPairing,
|
|
49
52
|
packageRoot,
|
|
50
53
|
packageVersion,
|
|
54
|
+
pairingCode,
|
|
51
55
|
pairingLink,
|
|
56
|
+
pairingName,
|
|
52
57
|
parseEnvFile,
|
|
53
|
-
parsePairingLink,
|
|
54
58
|
parseSetupFlags,
|
|
55
59
|
planCli,
|
|
56
60
|
planSetup,
|
|
61
|
+
providerOf,
|
|
62
|
+
readAgentPublicKey,
|
|
57
63
|
renderService,
|
|
58
64
|
resolveCodexHome,
|
|
59
65
|
resolveStateDir,
|
|
@@ -65,14 +71,15 @@ import {
|
|
|
65
71
|
systemPathResolver,
|
|
66
72
|
unitRunsLauncher,
|
|
67
73
|
withEnrolmentMode,
|
|
68
|
-
withLegacyEnv
|
|
69
|
-
|
|
74
|
+
withLegacyEnv,
|
|
75
|
+
workspaceLink
|
|
76
|
+
} from "./chunk-4M2ZRGQR.js";
|
|
70
77
|
|
|
71
78
|
// src/cli.ts
|
|
72
79
|
import { randomBytes } from "node:crypto";
|
|
73
80
|
import { homedir, hostname, userInfo } from "node:os";
|
|
74
|
-
import { readFile as readFile6, mkdir as
|
|
75
|
-
import { dirname as
|
|
81
|
+
import { readFile as readFile6, mkdir as mkdir3, realpath as realpath2 } from "node:fs/promises";
|
|
82
|
+
import { dirname as dirname3, join as joinPath, resolve as resolvePath } from "node:path";
|
|
76
83
|
|
|
77
84
|
// src/application/format-workspace-table.ts
|
|
78
85
|
var TITLE_MAX = 40;
|
|
@@ -653,29 +660,13 @@ async function firstOnPath(name, pathVar) {
|
|
|
653
660
|
return null;
|
|
654
661
|
}
|
|
655
662
|
|
|
656
|
-
// src/infrastructure/atomic-write.ts
|
|
657
|
-
import { chmod as chmod3, mkdir as mkdir3, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
658
|
-
import { dirname as dirname3 } from "node:path";
|
|
659
|
-
async function atomicWrite(path, text, mode) {
|
|
660
|
-
await mkdir3(dirname3(path), { recursive: true });
|
|
661
|
-
const temp = `${path}.tmp-${process.pid}`;
|
|
662
|
-
try {
|
|
663
|
-
await writeFile2(temp, text, { mode });
|
|
664
|
-
await chmod3(temp, mode);
|
|
665
|
-
await rename2(temp, path);
|
|
666
|
-
} catch (err) {
|
|
667
|
-
await rm2(temp, { force: true });
|
|
668
|
-
throw err;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
663
|
// src/infrastructure/agent-log.ts
|
|
673
664
|
import { open, readFile as readFile4, stat as stat3 } from "node:fs/promises";
|
|
674
665
|
import { join as join5 } from "node:path";
|
|
675
666
|
|
|
676
667
|
// src/infrastructure/service-install.ts
|
|
677
668
|
import { execFile as execFile2 } from "node:child_process";
|
|
678
|
-
import { chmod as
|
|
669
|
+
import { chmod as chmod3, cp, copyFile as copyFile3, readFile as readFile3, rm as rm2, stat as stat2 } from "node:fs/promises";
|
|
679
670
|
import { join as join4 } from "node:path";
|
|
680
671
|
import { promisify as promisify2 } from "node:util";
|
|
681
672
|
var exec2 = promisify2(execFile2);
|
|
@@ -705,7 +696,7 @@ async function backupFile(path) {
|
|
|
705
696
|
if (!await exists(path)) return null;
|
|
706
697
|
const copy = `${path}.bak-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
707
698
|
await copyFile3(path, copy);
|
|
708
|
-
await
|
|
699
|
+
await chmod3(copy, (await stat2(path)).mode & 511);
|
|
709
700
|
return copy;
|
|
710
701
|
}
|
|
711
702
|
var LABEL_GONE_MS = 1e4;
|
|
@@ -759,7 +750,7 @@ function serviceInstall(options) {
|
|
|
759
750
|
return [`launchctl bootout ${domain}/${LEGACY_LABEL}`];
|
|
760
751
|
},
|
|
761
752
|
async removeLegacy() {
|
|
762
|
-
await
|
|
753
|
+
await rm2(legacyPlist, { force: true });
|
|
763
754
|
return [`removed ${legacyPlist}`];
|
|
764
755
|
}
|
|
765
756
|
};
|
|
@@ -811,9 +802,9 @@ function serviceInstall(options) {
|
|
|
811
802
|
return ran.map(([command, args]) => `${command} ${args.join(" ")}`);
|
|
812
803
|
},
|
|
813
804
|
async removeLegacy() {
|
|
814
|
-
await
|
|
805
|
+
await rm2(legacyUnit, { force: true });
|
|
815
806
|
if (await exists(legacyDropIns) && !await exists(dropIns)) await cp(legacyDropIns, dropIns, { recursive: true });
|
|
816
|
-
await
|
|
807
|
+
await rm2(legacyDropIns, { recursive: true, force: true });
|
|
817
808
|
await run4("systemctl", ["--user", "daemon-reload"]);
|
|
818
809
|
return [`removed ${legacyUnit} and its drop-ins`, "systemctl --user daemon-reload"];
|
|
819
810
|
}
|
|
@@ -980,29 +971,50 @@ function attempt(port, ms) {
|
|
|
980
971
|
});
|
|
981
972
|
}
|
|
982
973
|
|
|
983
|
-
// src/
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
974
|
+
// src/infrastructure/read-secret.ts
|
|
975
|
+
async function readSecret(prompt) {
|
|
976
|
+
const stdin = process.stdin;
|
|
977
|
+
if (!stdin.isTTY) {
|
|
978
|
+
const chunks = [];
|
|
979
|
+
for await (const chunk of stdin) chunks.push(chunk);
|
|
980
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
981
|
+
}
|
|
982
|
+
process.stderr.write(prompt);
|
|
983
|
+
stdin.setRawMode(true);
|
|
984
|
+
stdin.resume();
|
|
985
|
+
stdin.setEncoding("utf8");
|
|
986
|
+
try {
|
|
987
|
+
return await new Promise((resolve2, reject) => {
|
|
988
|
+
let typed = "";
|
|
989
|
+
const onData = (data) => {
|
|
990
|
+
for (const ch of data) {
|
|
991
|
+
if (ch === "\r" || ch === "\n" || ch === "") {
|
|
992
|
+
stdin.off("data", onData);
|
|
993
|
+
resolve2(typed);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
if (ch === "") {
|
|
997
|
+
stdin.off("data", onData);
|
|
998
|
+
reject(new Error("cancelled"));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (ch === "\x7F" || ch === "\b") typed = typed.slice(0, -1);
|
|
1002
|
+
else typed += ch;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
stdin.on("data", onData);
|
|
1006
|
+
stdin.once("end", () => reject(new Error("cancelled")));
|
|
1007
|
+
});
|
|
1008
|
+
} finally {
|
|
1009
|
+
stdin.setRawMode(false);
|
|
1010
|
+
stdin.pause();
|
|
1011
|
+
process.stderr.write("\n");
|
|
994
1012
|
}
|
|
995
|
-
const secret = deps.newSecret();
|
|
996
|
-
await deps.pairings.update((list) => addPairing(list, secret, deps.now(), deps.ttlMs));
|
|
997
|
-
return pairingLink(input.pwaOrigin, {
|
|
998
|
-
urls: input.phoneUrls,
|
|
999
|
-
agentKey: deps.keys.x25519.publicKey,
|
|
1000
|
-
secret,
|
|
1001
|
-
name: deps.machineId,
|
|
1002
|
-
...deps.defaultPath !== void 0 ? { defaultPath: deps.defaultPath } : {}
|
|
1003
|
-
});
|
|
1004
1013
|
}
|
|
1005
1014
|
|
|
1015
|
+
// src/cli.ts
|
|
1016
|
+
import encodeQR from "qr";
|
|
1017
|
+
|
|
1006
1018
|
// src/infrastructure/agent-service.ts
|
|
1007
1019
|
import { execFile as execFile5 } from "node:child_process";
|
|
1008
1020
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -1186,7 +1198,7 @@ async function run3(argv, rawEnv, home, io = {}) {
|
|
|
1186
1198
|
throw new UsageError(RELAYS_GONE);
|
|
1187
1199
|
}
|
|
1188
1200
|
if (argv[0] === "pair") return pair(argv.slice(1), env, home);
|
|
1189
|
-
if (argv[0] === "ls" || argv[0] === "spawn" || argv[0] === "peek" || argv[0] === "unpark" || argv[0] === "rename" || argv[0] === "send" || argv[0] === "close" || argv[0] === "wait") {
|
|
1201
|
+
if (argv[0] === "ls" || argv[0] === "spawn" || argv[0] === "peek" || argv[0] === "unpark" || argv[0] === "rename" || argv[0] === "link" || argv[0] === "send" || argv[0] === "close" || argv[0] === "wait") {
|
|
1190
1202
|
return orchestration(argv[0], argv.slice(1), env, home, io);
|
|
1191
1203
|
}
|
|
1192
1204
|
const config = loadConfig({ ...env, ...await agentEnvValues(env, home) }, home);
|
|
@@ -1252,6 +1264,8 @@ async function run3(argv, rawEnv, home, io = {}) {
|
|
|
1252
1264
|
}
|
|
1253
1265
|
case "devices":
|
|
1254
1266
|
return devices(new FileDeviceStore(config.devicesPath), rest);
|
|
1267
|
+
case "dictation-key":
|
|
1268
|
+
return dictationKey(new FileDictationKeysStore(config.dictationKeysPath), rest, io.secret ?? readSecret);
|
|
1255
1269
|
default:
|
|
1256
1270
|
throw new UsageError(`unknown command: ${command} (run \`tarfa --help\`)`);
|
|
1257
1271
|
}
|
|
@@ -1260,8 +1274,9 @@ var HELP = `usage: tarfa <command> [options] (\`tarfa setup --help\`, \`tarfa
|
|
|
1260
1274
|
|
|
1261
1275
|
Set up this machine:
|
|
1262
1276
|
setup write agent.env, the agent key, the service and the hooks, then check the phone port
|
|
1263
|
-
pair print a one-time pairing
|
|
1277
|
+
pair print a one-time pairing code and QR code for a phone (--link: a link, for an older app)
|
|
1264
1278
|
devices list, revoke or forget paired phones; open or close enrolment
|
|
1279
|
+
dictation-key store the ElevenLabs or OpenRouter key every paired phone dictates with
|
|
1265
1280
|
update install the newest @tarfa/cli and apply it (package installs)
|
|
1266
1281
|
apply rewrite the launcher and service if they changed, reinstall the hooks, restart
|
|
1267
1282
|
install-hooks install the Claude Code and Codex hooks that report each workspace's status
|
|
@@ -1275,6 +1290,7 @@ Workspaces on this machine:
|
|
|
1275
1290
|
send send a message to another workspace
|
|
1276
1291
|
unpark reopen parked workspaces
|
|
1277
1292
|
rename rename a workspace (without an id: this one)
|
|
1293
|
+
link print a link that opens a workspace in the app (without an id: this one)
|
|
1278
1294
|
close close workspaces
|
|
1279
1295
|
list, create, recent, browse the agent's own views, as JSON
|
|
1280
1296
|
|
|
@@ -1287,12 +1303,14 @@ var COMMANDS = /* @__PURE__ */ new Set([
|
|
|
1287
1303
|
"connect",
|
|
1288
1304
|
"relay-key",
|
|
1289
1305
|
"devices",
|
|
1306
|
+
"dictation-key",
|
|
1290
1307
|
"install-hooks",
|
|
1291
1308
|
"ls",
|
|
1292
1309
|
"spawn",
|
|
1293
1310
|
"peek",
|
|
1294
1311
|
"unpark",
|
|
1295
1312
|
"rename",
|
|
1313
|
+
"link",
|
|
1296
1314
|
"send",
|
|
1297
1315
|
"close",
|
|
1298
1316
|
"wait",
|
|
@@ -1307,6 +1325,7 @@ var SPAWN_CLIS = ["claude", "codex", "pi"];
|
|
|
1307
1325
|
var PEEK_USAGE = "usage: tarfa peek <workspace id> [-n <lines>]";
|
|
1308
1326
|
var UNPARK_USAGE = "usage: tarfa unpark <workspace id>...";
|
|
1309
1327
|
var RENAME_USAGE = "usage: tarfa rename [<workspace id>] <title> (without an id: the workspace this runs in)";
|
|
1328
|
+
var LINK_USAGE = "usage: tarfa link [<workspace id>] [--pwa <PWA origin>] (without an id: the workspace this runs in)";
|
|
1310
1329
|
var SEND_USAGE = 'usage: tarfa send <workspace id> "<message>" (or the message on stdin)';
|
|
1311
1330
|
var CLOSE_USAGE = "usage: tarfa close <workspace id>...";
|
|
1312
1331
|
var WAIT_USAGE = "usage: tarfa wait <workspace id>... [--timeout <seconds, at least 10>]";
|
|
@@ -1469,6 +1488,18 @@ async function orchestration(command, argv, env, home, io) {
|
|
|
1469
1488
|
const done = `${id}: renamed to "${title.trim()}"`;
|
|
1470
1489
|
return locked ? done : `${done} (this tmux cannot lock a title, so Claude may rename it again)`;
|
|
1471
1490
|
}
|
|
1491
|
+
case "link": {
|
|
1492
|
+
const origin = pwaOrigin(takeOption(args, "--pwa", LINK_USAGE));
|
|
1493
|
+
if (args.length > 1 || args.some((a) => a.startsWith("-"))) throw new Error(LINK_USAGE);
|
|
1494
|
+
const id = args[0] ?? await callerWorkspace(env, tmux, config.sessionPrefix);
|
|
1495
|
+
const workspace = (await listAll()).find((w) => w.id === id);
|
|
1496
|
+
if (!workspace) throw new Error(`no workspace ${id} (see tarfa ls)`);
|
|
1497
|
+
if (isHiddenPath(workspace.projectPath, await new FileHiddenFoldersStore(config.hiddenFoldersPath).list())) {
|
|
1498
|
+
throw new Error(`${id} is in a hidden folder, which phones do not list`);
|
|
1499
|
+
}
|
|
1500
|
+
const agentKey = await readAgentPublicKey(config.agentKeyPath);
|
|
1501
|
+
return workspaceLink(origin ?? pwaOrigin(config.pwaOrigin), { agentKey, workspaceId: id });
|
|
1502
|
+
}
|
|
1472
1503
|
case "close": {
|
|
1473
1504
|
if (args.length === 0 || args.some((id) => id.startsWith("-"))) throw new UsageError(CLOSE_USAGE);
|
|
1474
1505
|
const self = await callerWorkspace(env, tmux, config.sessionPrefix).catch(() => void 0);
|
|
@@ -1587,7 +1618,8 @@ async function readStdin(ttyError) {
|
|
|
1587
1618
|
return Buffer.concat(chunks).toString("utf8");
|
|
1588
1619
|
}
|
|
1589
1620
|
var RELAYS_GONE = "relays are gone: tarfa setup sets a machine up for phones, and tarfa pair pairs one";
|
|
1590
|
-
var
|
|
1621
|
+
var MAX_PAIRING_NAME = 64;
|
|
1622
|
+
var PAIR_USAGE = `usage: tarfa pair [--pwa <PWA origin>] [--ttl <minutes, 1-${PAIRING_TTL_MS / 6e4}, default ${PAIRING_TTL_MS / 6e4}>] [--link]`;
|
|
1591
1623
|
function takeOption(args, name, usage, Failure = Error) {
|
|
1592
1624
|
const at = args.indexOf(name);
|
|
1593
1625
|
if (at === -1) return void 0;
|
|
@@ -1629,6 +1661,8 @@ async function pair(argv, env, home) {
|
|
|
1629
1661
|
const config = loadConfig({ ...env, ...await agentEnvValues(env, home) }, home);
|
|
1630
1662
|
let origin = pwaOrigin(takeOption(args, "--pwa", PAIR_USAGE));
|
|
1631
1663
|
const ttl = takeOption(args, "--ttl", PAIR_USAGE) ?? String(PAIRING_TTL_MS / 6e4);
|
|
1664
|
+
const linkAt = args.indexOf("--link");
|
|
1665
|
+
if (linkAt !== -1) args.splice(linkAt, 1);
|
|
1632
1666
|
const minutes = Number(ttl);
|
|
1633
1667
|
if (!/^\d+$/.test(ttl) || minutes < 1 || minutes * 6e4 > PAIRING_TTL_MS) throw new Error(`--ttl takes whole minutes from 1 to ${PAIRING_TTL_MS / 6e4} (got ${ttl})`);
|
|
1634
1668
|
if (args.length > 0) throw new Error(PAIR_USAGE);
|
|
@@ -1643,19 +1677,21 @@ async function pair(argv, env, home) {
|
|
|
1643
1677
|
const bad = config.phoneAddresses.find((url) => !isMachineUrl(url));
|
|
1644
1678
|
if (bad) throw new Error(`TARFA_PHONE_ADDRESSES holds ${bad}, which a phone refuses: wss://<host>[:port][/<slug>] (run tarfa setup)`);
|
|
1645
1679
|
if (config.phoneAddresses.length > MAX_ADDRESSES) throw new Error(`TARFA_PHONE_ADDRESSES holds more than ${MAX_ADDRESSES} addresses`);
|
|
1646
|
-
const
|
|
1680
|
+
const machineId = pairingName(config.machineId, hostname());
|
|
1681
|
+
if (machineId.trim().length > MAX_PAIRING_NAME) throw new Error(`TARFA_MACHINE_ID must be at most ${MAX_PAIRING_NAME} characters, the name phones show (got ${machineId.length})`);
|
|
1682
|
+
const pairing = await mintPairing(
|
|
1647
1683
|
{
|
|
1648
1684
|
keys: await loadOrCreateAgentKeys(config.agentKeyPath),
|
|
1649
|
-
machineId
|
|
1685
|
+
machineId,
|
|
1650
1686
|
...config.defaultPath ? { defaultPath: config.defaultPath } : {},
|
|
1651
1687
|
pairings: new FilePairingStore(config.pairingPath),
|
|
1652
1688
|
newSecret: generatePairingSecret,
|
|
1653
1689
|
now: () => /* @__PURE__ */ new Date(),
|
|
1654
1690
|
ttlMs: minutes * 6e4
|
|
1655
1691
|
},
|
|
1656
|
-
{ phoneUrls: config.phoneAddresses
|
|
1692
|
+
{ phoneUrls: config.phoneAddresses }
|
|
1657
1693
|
);
|
|
1658
|
-
return pairingInstructions(
|
|
1694
|
+
return pairingInstructions(pairing, { origin, minutes, link: linkAt !== -1 });
|
|
1659
1695
|
}
|
|
1660
1696
|
var WaitTimeout = class extends Error {
|
|
1661
1697
|
exitCode = 124;
|
|
@@ -1675,11 +1711,16 @@ replace anything it did not make (an agent key, another tailscale serve entry).
|
|
|
1675
1711
|
|
|
1676
1712
|
--machine-id <id> the name phones show for this machine
|
|
1677
1713
|
--project-roots <dir,dir> absolute folders new workspaces may be made in
|
|
1678
|
-
--default-path <dir> where a phone resolves relative paths (
|
|
1714
|
+
--default-path <dir> where a phone resolves relative paths (sent when pairing)
|
|
1679
1715
|
--phone-port <n> loopback port phones reach (default 8797); every Linux user on one
|
|
1680
1716
|
machine needs their own, e.g. 8797 and 8798
|
|
1681
|
-
--tailscale serve|
|
|
1682
|
-
|
|
1717
|
+
--tailscale funnel|serve|none how phones reach this machine: with or without Tailscale (funnel,
|
|
1718
|
+
public through Funnel), only with Tailscale on (serve), or through an
|
|
1719
|
+
address published some other way (none, with --address). Ask the
|
|
1720
|
+
operator. Without it, setup keeps the TARFA_PHONE_ADDRESSES or the
|
|
1721
|
+
serve handler for the phone port the machine already has, picks
|
|
1722
|
+
funnel for a machine phones reached through a relay, and otherwise
|
|
1723
|
+
refuses. Every run prints a reachability: line saying which
|
|
1683
1724
|
--tailscale-port <n> the HTTPS port Tailscale serves it on (default 443 for serve, 10000
|
|
1684
1725
|
for funnel; Funnel allows only 443, 8443 and 10000)
|
|
1685
1726
|
--slug <name> serve under /<name>, so several agents share one port
|
|
@@ -1687,7 +1728,7 @@ replace anything it did not make (an agent key, another tailscale serve entry).
|
|
|
1687
1728
|
4 in all), e.g. a reverse proxy
|
|
1688
1729
|
--pwa <origin> where phones open the app (default https://tarfa.app; an existing
|
|
1689
1730
|
TARFA_PWA_ORIGIN is kept unless this is given)
|
|
1690
|
-
--pair print a pairing
|
|
1731
|
+
--pair print a pairing code and QR code at the end
|
|
1691
1732
|
--adopt a machine whose service runs a clone's deploy/run-agent.sh: move it to
|
|
1692
1733
|
the installed package (copies deploy/agent.env to ~/.tarfa/agent.env,
|
|
1693
1734
|
keeps the original, repoints the service, replaces the ~/.local/bin links
|
|
@@ -1795,9 +1836,10 @@ ${SETUP_USAGE.split("\n")[0]}
|
|
|
1795
1836
|
stateDir: stateDirLayout(home),
|
|
1796
1837
|
phonePortOwner: await owner(flags.phonePort),
|
|
1797
1838
|
hooksPortOwner: await owner(config.port),
|
|
1798
|
-
tailscale:
|
|
1799
|
-
//
|
|
1800
|
-
|
|
1839
|
+
// Read whatever --tailscale says: they settle how the port is published when no flag does, and word
|
|
1840
|
+
// whether phones need Tailscale. Both answer null when Tailscale is absent or will not tell.
|
|
1841
|
+
tailscale: await tailscale.status(),
|
|
1842
|
+
serve: await tailscale.serveStatus(),
|
|
1801
1843
|
cli: kind === "package" ? await cliState(home, env) : null,
|
|
1802
1844
|
adoption: cloneRepo === null ? null : { repo: cloneRepo, copyEnv, agentLive: { path: agentLivePath, kind: await linkKind(agentLivePath) } }
|
|
1803
1845
|
};
|
|
@@ -1813,7 +1855,7 @@ ${SETUP_USAGE.split("\n")[0]}
|
|
|
1813
1855
|
await loadOrCreateAgentKeys(config.agentKeyPath);
|
|
1814
1856
|
},
|
|
1815
1857
|
writeRoster: async () => {
|
|
1816
|
-
await
|
|
1858
|
+
await mkdir3(dirname3(config.devicesPath), { recursive: true, mode: 448 });
|
|
1817
1859
|
await devices2.update((roster) => roster);
|
|
1818
1860
|
},
|
|
1819
1861
|
writeLauncher: (path, text) => atomicWrite(path, text, 493),
|
|
@@ -1946,20 +1988,52 @@ async function apply2(argv, env, home, io) {
|
|
|
1946
1988
|
if (!result.ok) throw new Error(result.lines.join("\n"));
|
|
1947
1989
|
return result.lines.join("\n");
|
|
1948
1990
|
}
|
|
1949
|
-
function pairingInstructions(
|
|
1950
|
-
|
|
1991
|
+
function pairingInstructions(pairing, options) {
|
|
1992
|
+
const { origin, minutes } = options;
|
|
1993
|
+
const text = options.link ? pairingLink(origin, pairing) : pairingCode(pairing);
|
|
1994
|
+
const how = options.link ? [
|
|
1951
1995
|
"On the phone, in Tarfa: Settings \u2192 Scan QR code, then scan this. Or open the link below on the",
|
|
1952
|
-
"phone (Android opens the installed app; on an iPhone copy it and use Settings \u2192 Paste pairing link)."
|
|
1996
|
+
"phone (Android opens the installed app; on an iPhone copy it and use Settings \u2192 Paste pairing link)."
|
|
1997
|
+
] : [
|
|
1998
|
+
`On the phone, open the Tarfa app (install it from ${origin} first), then Settings \u2192 Add machine:`,
|
|
1999
|
+
"Scan QR code and scan this, or copy the code below to the phone and tap Paste pairing code."
|
|
2000
|
+
];
|
|
2001
|
+
return [
|
|
2002
|
+
...how,
|
|
1953
2003
|
"",
|
|
1954
2004
|
// Half-height blocks: about 60 columns, where `term` needs twice that.
|
|
1955
|
-
encodeQR(
|
|
1956
|
-
|
|
2005
|
+
encodeQR(text, "ascii", { ecc: "low" }),
|
|
2006
|
+
text,
|
|
1957
2007
|
"",
|
|
1958
2008
|
// The phone asks before pairing and shows this fingerprint: they must match.
|
|
1959
|
-
`The phone will show machine key ${deviceFingerprint(
|
|
2009
|
+
`The phone will show machine key ${deviceFingerprint(pairing.agentKey)}; check it matches.`,
|
|
1960
2010
|
`It works once, for ${minutes} minute${minutes === 1 ? "" : "s"}. \`tarfa pair\` makes another.`
|
|
1961
2011
|
].join("\n");
|
|
1962
2012
|
}
|
|
2013
|
+
var DICTATION_KEY_USAGE = [
|
|
2014
|
+
`usage: tarfa dictation-key [status | set ${DICTATION_PROVIDERS.join("|")} | remove ${DICTATION_PROVIDERS.join("|")}]`,
|
|
2015
|
+
" set reads the key on stdin, or asks for it with echo off; never as an argument",
|
|
2016
|
+
" every phone paired with this machine dictates into its workspaces with it, at its next connect"
|
|
2017
|
+
].join("\n");
|
|
2018
|
+
async function dictationKey(store, [action = "status", name, ...extra], secret) {
|
|
2019
|
+
if (action === "status") {
|
|
2020
|
+
if (name !== void 0) throw new UsageError(DICTATION_KEY_USAGE);
|
|
2021
|
+
const loaded2 = await store.load();
|
|
2022
|
+
if ("problem" in loaded2) throw new Error(loaded2.problem);
|
|
2023
|
+
return DICTATION_PROVIDERS.map((p) => `${p} ${loaded2.keys[p] ? "set" : "not set"}`).join("\n");
|
|
2024
|
+
}
|
|
2025
|
+
if (action !== "set" && action !== "remove") throw new UsageError(DICTATION_KEY_USAGE);
|
|
2026
|
+
if (action === "set" && extra.length > 0) {
|
|
2027
|
+
throw new UsageError("pass the key on stdin or type it at the prompt, never as an argument: any local user can read a process's arguments");
|
|
2028
|
+
}
|
|
2029
|
+
const provider = name ? providerOf(name) : null;
|
|
2030
|
+
if (!provider || extra.length > 0) throw new UsageError(DICTATION_KEY_USAGE);
|
|
2031
|
+
const loaded = await store.load();
|
|
2032
|
+
if ("problem" in loaded) throw new Error(loaded.problem);
|
|
2033
|
+
const result = await changeDictationKey(store, provider, action === "remove" ? null : await secret(`${provider} key: `));
|
|
2034
|
+
if (!result.ok) throw new Error(result.error);
|
|
2035
|
+
return action === "remove" ? `${provider}: removed; phones stop getting it at their next connect. Rotate it at the provider if a phone that had it is lost` : `${provider}: stored; every phone paired with this machine gets it at its next connect`;
|
|
2036
|
+
}
|
|
1963
2037
|
var DEVICES_USAGE = [
|
|
1964
2038
|
"usage: tarfa devices [list | revoke <id|key> | forget <id|key> | enrolment open|closed]",
|
|
1965
2039
|
" enrolment open a pairing code from `tarfa pair` enrols a new phone",
|
package/dist/serve.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DEVICE_REFUSED,
|
|
7
7
|
FileClaudeSessionsStore,
|
|
8
8
|
FileDeviceStore,
|
|
9
|
+
FileDictationKeysStore,
|
|
9
10
|
FileHiddenFoldersStore,
|
|
10
11
|
FilePairingStore,
|
|
11
12
|
FileParkedStore,
|
|
@@ -25,9 +26,11 @@ import {
|
|
|
25
26
|
ShortWorkspaceIds,
|
|
26
27
|
TmuxAdapter,
|
|
27
28
|
WrongStaticKeyError,
|
|
29
|
+
announcedAddresses,
|
|
28
30
|
audioMediaType,
|
|
29
31
|
b64url,
|
|
30
32
|
browseDir,
|
|
33
|
+
changeDictationKey,
|
|
31
34
|
childEnv,
|
|
32
35
|
cliOfCommand,
|
|
33
36
|
configWarnings,
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
enrolDevice,
|
|
40
43
|
enrolmentMode,
|
|
41
44
|
exactTarget,
|
|
45
|
+
exposedFiles,
|
|
42
46
|
formatAgentMessage,
|
|
43
47
|
fsIsDirectory,
|
|
44
48
|
getRecentPaths,
|
|
@@ -74,7 +78,7 @@ import {
|
|
|
74
78
|
systemPathResolver,
|
|
75
79
|
withLegacyEnv,
|
|
76
80
|
writeFileAtomic
|
|
77
|
-
} from "./chunk-
|
|
81
|
+
} from "./chunk-4M2ZRGQR.js";
|
|
78
82
|
|
|
79
83
|
// src/serve.ts
|
|
80
84
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -1284,6 +1288,7 @@ var PRE_AUTH_MAX_MESSAGE_BYTES = 8 * 1024;
|
|
|
1284
1288
|
var MAX_AUTH_ATTEMPTS = 3;
|
|
1285
1289
|
var MAX_HELD_MESSAGES = 16;
|
|
1286
1290
|
var PAIRING_SECRET = /^[A-Za-z0-9_-]{43}$/;
|
|
1291
|
+
var FEATURES = [AGENT_FEATURES.lastReply, AGENT_FEATURES.dictationKeys, AGENT_FEATURES.setDictationKey];
|
|
1287
1292
|
var NoiseClientConnection = class {
|
|
1288
1293
|
constructor(socket, staticPrivate, options) {
|
|
1289
1294
|
this.socket = socket;
|
|
@@ -1332,7 +1337,15 @@ var NoiseClientConnection = class {
|
|
|
1332
1337
|
return;
|
|
1333
1338
|
}
|
|
1334
1339
|
if (message.type === "authResult" && message.ok) {
|
|
1335
|
-
const
|
|
1340
|
+
const { agentVersion: agentVersion2, phoneAddresses } = this.options;
|
|
1341
|
+
const features = [...this.options.features ?? FEATURES];
|
|
1342
|
+
const agent = {
|
|
1343
|
+
protocol: PROTOCOL_VERSION,
|
|
1344
|
+
...agentVersion2 ? { version: agentVersion2 } : {},
|
|
1345
|
+
features: phoneAddresses ? [...features, AGENT_FEATURES.addresses] : features,
|
|
1346
|
+
...phoneAddresses ? { addresses: phoneAddresses } : {}
|
|
1347
|
+
};
|
|
1348
|
+
const accepted = { ...message, agent };
|
|
1336
1349
|
message = accepted;
|
|
1337
1350
|
if (!this.awaitingPhone) this.cancelDeadline();
|
|
1338
1351
|
this.session.raiseCap(MAX_MESSAGE_BYTES);
|
|
@@ -2386,6 +2399,12 @@ var ConnectionHandler = class {
|
|
|
2386
2399
|
this.conn.send({ type: "keyterms", keyterms: await this.deps.keyterms.list() });
|
|
2387
2400
|
return;
|
|
2388
2401
|
}
|
|
2402
|
+
case "getDictationKeys":
|
|
2403
|
+
this.conn.send({ type: "dictationKeys", ...await this.deps.dictationKeys?.read() });
|
|
2404
|
+
return;
|
|
2405
|
+
case "setDictationKey":
|
|
2406
|
+
this.conn.send({ type: "dictationKeySet", id: message.id, provider: message.provider, ...await this.setDictationKey(message.provider, message.key) });
|
|
2407
|
+
return;
|
|
2389
2408
|
case "readFile": {
|
|
2390
2409
|
const { binary, data, truncated } = await readFile10({ reader }, message.path, await this.allowedRoots());
|
|
2391
2410
|
this.conn.send({ type: "fileContents", path: message.path, data, truncated, binary });
|
|
@@ -2679,6 +2698,17 @@ var ConnectionHandler = class {
|
|
|
2679
2698
|
});
|
|
2680
2699
|
this.conn.send({ type: "attached", workspaceId });
|
|
2681
2700
|
}
|
|
2701
|
+
// Any enrolled phone of this machine may change its key: it can already run anything in a
|
|
2702
|
+
// terminal here. A thrown error may name the file, never the key, but is worded here anyway.
|
|
2703
|
+
async setDictationKey(provider, key) {
|
|
2704
|
+
const store = this.deps.dictationKeys;
|
|
2705
|
+
if (!store) return { ok: false, error: "this agent keeps no dictation keys" };
|
|
2706
|
+
try {
|
|
2707
|
+
return await changeDictationKey(store, provider, key);
|
|
2708
|
+
} catch {
|
|
2709
|
+
return { ok: false, error: "the keys file could not be written" };
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2682
2712
|
// A dictation lives only as long as its attachment: without one the held spaces go nowhere,
|
|
2683
2713
|
// and the machine's one dictation slot would stay taken.
|
|
2684
2714
|
cancelVoice(reason) {
|
|
@@ -3876,7 +3906,7 @@ function startDeviceGate(deps) {
|
|
|
3876
3906
|
return {
|
|
3877
3907
|
staticPrivate: b64url.decode(keys.x25519.privateKey),
|
|
3878
3908
|
devices,
|
|
3879
|
-
options: (extra) => ({ devices, log, refusalLog, pushBudget, agentVersion: deps.agentVersion, ...extra }),
|
|
3909
|
+
options: (extra) => ({ devices, log, refusalLog, pushBudget, agentVersion: deps.agentVersion, phoneAddresses: deps.phoneAddresses, ...extra }),
|
|
3880
3910
|
roster: () => last,
|
|
3881
3911
|
pollRoster: () => monitor.poll(),
|
|
3882
3912
|
stop: () => clearInterval(poll)
|
|
@@ -4127,13 +4157,6 @@ import { join as join6 } from "node:path";
|
|
|
4127
4157
|
|
|
4128
4158
|
// src/infrastructure/private-dir.ts
|
|
4129
4159
|
import { chmod, mkdir as mkdir7, stat as stat4 } from "node:fs/promises";
|
|
4130
|
-
|
|
4131
|
-
// src/application/private-files.ts
|
|
4132
|
-
function exposedFiles(files) {
|
|
4133
|
-
return files.filter(({ mode }) => (mode & 63) !== 0).map(({ path, mode }) => `${path} is readable or writable by other users (mode ${(mode & 511).toString(8)}): run \`chmod 600 ${path}\``);
|
|
4134
|
-
}
|
|
4135
|
-
|
|
4136
|
-
// src/infrastructure/private-dir.ts
|
|
4137
4160
|
async function securePrivateDir(dir) {
|
|
4138
4161
|
await mkdir7(dir, { recursive: true, mode: 448 });
|
|
4139
4162
|
await chmod(dir, 448);
|
|
@@ -4196,6 +4219,7 @@ async function main() {
|
|
|
4196
4219
|
config.devicesPath,
|
|
4197
4220
|
config.pairingPath,
|
|
4198
4221
|
join7(config.stateDir, "token"),
|
|
4222
|
+
config.dictationKeysPath,
|
|
4199
4223
|
// The launcher sources it: whoever can write it runs code as this user.
|
|
4200
4224
|
await locateAgentEnv(env, homedir2())
|
|
4201
4225
|
]);
|
|
@@ -4238,6 +4262,8 @@ async function main() {
|
|
|
4238
4262
|
launchers: new FileLaunchersStore(config.launchersPath),
|
|
4239
4263
|
detectInstalled: (names) => detectInstalled(names, loginShellResolver()),
|
|
4240
4264
|
keyterms: new FileKeytermsStore(config.keytermsPath),
|
|
4265
|
+
dictationKeys: new FileDictationKeysStore(config.dictationKeysPath, (problem) => process.stderr.write(`tarfa agent: no dictation keys sent: ${problem}
|
|
4266
|
+
`)),
|
|
4241
4267
|
clientLog: new FileClientLogStore(config.clientLogPath),
|
|
4242
4268
|
hookEventLog: new FileHookEventLog(config.hookEventLogPath),
|
|
4243
4269
|
messaging: {
|
|
@@ -4277,7 +4303,8 @@ async function main() {
|
|
|
4277
4303
|
pairings: new FilePairingStore(config.pairingPath),
|
|
4278
4304
|
log,
|
|
4279
4305
|
rosterPollMs: ROSTER_POLL_MS,
|
|
4280
|
-
agentVersion
|
|
4306
|
+
agentVersion,
|
|
4307
|
+
phoneAddresses: announcedAddresses(config.phoneAddresses)
|
|
4281
4308
|
});
|
|
4282
4309
|
try {
|
|
4283
4310
|
const phones = await startPhoneListener({
|