@tarfa/cli 0.1.2 → 0.1.3
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 +37 -0
- package/dist/{chunk-XAYVIDCU.js → chunk-X6FKPFHV.js} +10 -1
- package/dist/cli.js +51 -3
- package/dist/serve.js +34 -5
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @tarfa/cli
|
|
2
|
+
|
|
3
|
+
The Tarfa agent and command line. Tarfa is a phone app ([tarfa.app](https://tarfa.app)) that shows
|
|
4
|
+
and drives the Claude Code, Codex and shell sessions (tmux workspaces) running on your machines.
|
|
5
|
+
This package runs on each machine (macOS or Linux): it serves that machine's workspaces to your
|
|
6
|
+
phone, end to end encrypted, and gives every workspace the `tarfa` command.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Needs `tmux` and [mise](https://mise.jdx.dev). Nothing is compiled.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
mise use -g node@24 npm:@tarfa/cli@latest
|
|
14
|
+
tarfa setup --machine-id <name> --project-roots ~/workspace --tailscale serve --dry-run # see the plan
|
|
15
|
+
tarfa setup --machine-id <name> --project-roots ~/workspace --tailscale serve --pair # run it
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`tarfa setup` writes `~/.tarfa/agent.env`, the agent key, the service (a systemd user unit or a
|
|
19
|
+
launchd agent) and the Claude Code hooks, publishes the phone port with Tailscale, and prints a
|
|
20
|
+
pairing link and QR code. Open it on the phone, inside the installed app. `tarfa setup --help`
|
|
21
|
+
lists every flag.
|
|
22
|
+
|
|
23
|
+
## Day to day
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
tarfa pair # a one-time pairing link for another phone
|
|
27
|
+
tarfa devices # paired phones; revoke or forget one
|
|
28
|
+
tarfa update # install the newest release and restart the agent; workspaces survive
|
|
29
|
+
tarfa ls # this machine's workspaces
|
|
30
|
+
tarfa --help # every command
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## More
|
|
34
|
+
|
|
35
|
+
The full runbook, including publishing the phone port without Tailscale and running your own copy
|
|
36
|
+
of the app, is [docs/DEPLOY.md](https://github.com/abumalick/tarfa/blob/main/docs/DEPLOY.md).
|
|
37
|
+
Source and issues: [github.com/abumalick/tarfa](https://github.com/abumalick/tarfa).
|
|
@@ -5416,6 +5416,9 @@ var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5416
5416
|
z6.object({ type: z6.literal("voiceStart"), workspaceId: z6.string(), id: z6.string() }),
|
|
5417
5417
|
z6.object({ type: z6.literal("voiceChunk"), id: z6.string(), data: z6.string() }),
|
|
5418
5418
|
z6.object({ type: z6.literal("voiceEnd"), id: z6.string() }),
|
|
5419
|
+
// The text of Claude's last reply in a workspace, read aloud on the phone. Sent only to an agent
|
|
5420
|
+
// whose authResult names the `lastReply` feature.
|
|
5421
|
+
z6.object({ type: z6.literal("lastReply"), workspaceId: z6.string() }),
|
|
5419
5422
|
z6.object({ type: z6.literal("resize"), workspaceId: z6.string(), cols: dim, rows: dim }),
|
|
5420
5423
|
// `remember` is set only when the command came from the Custom field: the agent then adds it
|
|
5421
5424
|
// to the machine's launcher list. Additive, so an older agent just ignores it.
|
|
@@ -5437,6 +5440,7 @@ var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5437
5440
|
]);
|
|
5438
5441
|
var detachReasonSchema = z6.enum(["opened-elsewhere", "closed", "error"]);
|
|
5439
5442
|
var PROTOCOL_VERSION = 1;
|
|
5443
|
+
var AGENT_FEATURES = { lastReply: "lastReply" };
|
|
5440
5444
|
var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
5441
5445
|
// `credential` is what authenticated the session; `enrolled` says the device key is enrolled (written), so the
|
|
5442
5446
|
// phone forgets its token and pairing secret. Both optional: an older agent sends neither.
|
|
@@ -5448,7 +5452,9 @@ var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5448
5452
|
credential: z6.enum(["device", "pairing", "token"]).optional().catch(void 0),
|
|
5449
5453
|
enrolled: z6.boolean().optional().catch(void 0),
|
|
5450
5454
|
pushed: z6.literal(true).optional().catch(void 0),
|
|
5451
|
-
agent
|
|
5455
|
+
// `features` names what this agent answers beyond the protocol, so the PWA offers a control
|
|
5456
|
+
// only to an agent that supports it (a worktree agent shares a release's version number).
|
|
5457
|
+
agent: z6.object({ protocol: z6.number().int(), version: z6.string().optional(), features: z6.array(z6.string()).optional().catch(void 0) }).optional().catch(void 0)
|
|
5452
5458
|
}),
|
|
5453
5459
|
// `claudeVoice`: whether this machine can play a Claude voice dictation (see voiceStart).
|
|
5454
5460
|
// An older agent omits it, and the PWA then treats Claude voice as unavailable there.
|
|
@@ -5484,6 +5490,8 @@ var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
|
5484
5490
|
z6.object({ type: z6.literal("attached"), workspaceId: z6.string() }),
|
|
5485
5491
|
z6.object({ type: z6.literal("output"), workspaceId: z6.string(), data: z6.string() }),
|
|
5486
5492
|
z6.object({ type: z6.literal("fileStored"), workspaceId: z6.string(), path: z6.string() }),
|
|
5493
|
+
// `text` is null when the agent holds no reply for that workspace (none since it started, or not Claude).
|
|
5494
|
+
z6.object({ type: z6.literal("lastReply"), workspaceId: z6.string(), text: z6.string().nullable() }),
|
|
5487
5495
|
z6.object({ type: z6.literal("voiceDone"), id: z6.string(), ok: z6.boolean(), error: z6.string().optional() }),
|
|
5488
5496
|
z6.object({ type: z6.literal("detached"), workspaceId: z6.string(), reason: detachReasonSchema }),
|
|
5489
5497
|
z6.object({ type: z6.literal("closed"), workspaceId: z6.string() }),
|
|
@@ -6127,6 +6135,7 @@ export {
|
|
|
6127
6135
|
pdfMediaType,
|
|
6128
6136
|
audioMediaType,
|
|
6129
6137
|
PROTOCOL_VERSION,
|
|
6138
|
+
AGENT_FEATURES,
|
|
6130
6139
|
parseClientMessage,
|
|
6131
6140
|
parseBrowserClientMessage,
|
|
6132
6141
|
parseBrowserAgentMessage,
|
package/dist/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
unitRunsLauncher,
|
|
64
64
|
withEnrolmentMode,
|
|
65
65
|
withLegacyEnv
|
|
66
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-X6FKPFHV.js";
|
|
67
67
|
|
|
68
68
|
// src/cli.ts
|
|
69
69
|
import { randomBytes } from "node:crypto";
|
|
@@ -1197,7 +1197,10 @@ async function cliState(home, env) {
|
|
|
1197
1197
|
var CODEX_NEXT_STEP = "open Codex and trust the Tarfa hooks in /hooks; repeat after any hook change";
|
|
1198
1198
|
async function run3(argv, rawEnv, home, io = {}) {
|
|
1199
1199
|
const env = withLegacyEnv(rawEnv);
|
|
1200
|
+
if (argv[0] === void 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") return HELP;
|
|
1200
1201
|
if (argv[0] === "--version" || argv[0] === "-v") return packageVersion();
|
|
1202
|
+
if (argv[0] === "serve") throw new UsageError("usage: tarfa serve (no options: runs the agent in the foreground, as the service does)");
|
|
1203
|
+
if (!COMMANDS.has(argv[0])) throw new UsageError(`unknown command: ${argv[0]} (run \`tarfa --help\`)`);
|
|
1201
1204
|
if (argv[0] === "setup") return setup(argv.slice(1), env, home, io);
|
|
1202
1205
|
if (argv[0] === "apply") return apply2(argv.slice(1), env, home, io);
|
|
1203
1206
|
if (argv[0] === "update") return update(argv.slice(1), env, home, io);
|
|
@@ -1272,9 +1275,54 @@ async function run3(argv, rawEnv, home, io = {}) {
|
|
|
1272
1275
|
case "devices":
|
|
1273
1276
|
return devices(new FileDeviceStore(config.devicesPath), rest);
|
|
1274
1277
|
default:
|
|
1275
|
-
throw new
|
|
1278
|
+
throw new UsageError(`unknown command: ${command} (run \`tarfa --help\`)`);
|
|
1276
1279
|
}
|
|
1277
1280
|
}
|
|
1281
|
+
var HELP = `usage: tarfa <command> [options] (\`tarfa setup --help\`, \`tarfa apply --help\` and \`tarfa update --help\` list their flags)
|
|
1282
|
+
|
|
1283
|
+
Set up this machine:
|
|
1284
|
+
setup write agent.env, the agent key, the service and the hooks, then check the phone port
|
|
1285
|
+
pair print a one-time pairing link and QR code for a phone
|
|
1286
|
+
devices list, revoke or forget paired phones; switch enrolment on, off or closed
|
|
1287
|
+
update install the newest @tarfa/cli and apply it (package installs)
|
|
1288
|
+
apply rewrite the launcher and service if they changed, reinstall the hooks, restart
|
|
1289
|
+
install-hooks install the Claude Code and Codex hooks that report each workspace's status
|
|
1290
|
+
serve run the agent in the foreground (what the service runs)
|
|
1291
|
+
|
|
1292
|
+
Workspaces on this machine:
|
|
1293
|
+
ls list workspaces and their state
|
|
1294
|
+
spawn open a workspace on a project with a brief on stdin (--child to orchestrate it)
|
|
1295
|
+
peek print the end of a workspace's screen
|
|
1296
|
+
wait wait until one of the workspaces stops working
|
|
1297
|
+
send send a message to another workspace
|
|
1298
|
+
unpark reopen parked workspaces
|
|
1299
|
+
rename rename a workspace (without an id: this one)
|
|
1300
|
+
close close workspaces
|
|
1301
|
+
list, create, recent, browse the agent's own views, as JSON
|
|
1302
|
+
|
|
1303
|
+
--version print the version`;
|
|
1304
|
+
var COMMANDS = /* @__PURE__ */ new Set([
|
|
1305
|
+
"setup",
|
|
1306
|
+
"apply",
|
|
1307
|
+
"update",
|
|
1308
|
+
"pair",
|
|
1309
|
+
"connect",
|
|
1310
|
+
"relay-key",
|
|
1311
|
+
"devices",
|
|
1312
|
+
"install-hooks",
|
|
1313
|
+
"ls",
|
|
1314
|
+
"spawn",
|
|
1315
|
+
"peek",
|
|
1316
|
+
"unpark",
|
|
1317
|
+
"rename",
|
|
1318
|
+
"send",
|
|
1319
|
+
"close",
|
|
1320
|
+
"wait",
|
|
1321
|
+
"list",
|
|
1322
|
+
"create",
|
|
1323
|
+
"recent",
|
|
1324
|
+
"browse"
|
|
1325
|
+
]);
|
|
1278
1326
|
var LS_USAGE = `usage: tarfa ls [--json] [--mine] [--parent <workspace id>] [--role ${WORKSPACE_ROLES.join("|")}] [--path <project dir>]`;
|
|
1279
1327
|
var SPAWN_USAGE = "usage: tarfa spawn [--title <title>] [--child] [--model <model>] [--cli claude|codex|pi] <projectPath> (brief on stdin)";
|
|
1280
1328
|
var SPAWN_CLIS = ["claude", "codex", "pi"];
|
|
@@ -1981,7 +2029,7 @@ function formatRoster(roster) {
|
|
|
1981
2029
|
}
|
|
1982
2030
|
if (isEntrypoint(import.meta.url, process.argv[1])) {
|
|
1983
2031
|
const argv = process.argv.slice(2);
|
|
1984
|
-
const program = argv[0] === "serve" ? import("./serve.js").then((agent) => agent.main()) : run3(argv, process.env, homedir()).then((out) => {
|
|
2032
|
+
const program = argv[0] === "serve" && argv.length === 1 ? import("./serve.js").then((agent) => agent.main()) : run3(argv, process.env, homedir()).then((out) => {
|
|
1985
2033
|
process.stdout.write(`${out}
|
|
1986
2034
|
`);
|
|
1987
2035
|
});
|
package/dist/serve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
AGENT_FEATURES,
|
|
3
4
|
CodexAppServer,
|
|
4
5
|
CodexAppServerDown,
|
|
5
6
|
DEVICE_REFUSED,
|
|
@@ -74,7 +75,7 @@ import {
|
|
|
74
75
|
systemPathResolver,
|
|
75
76
|
withLegacyEnv,
|
|
76
77
|
writeFileAtomic
|
|
77
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-X6FKPFHV.js";
|
|
78
79
|
|
|
79
80
|
// src/serve.ts
|
|
80
81
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -1332,7 +1333,7 @@ var NoiseClientConnection = class {
|
|
|
1332
1333
|
return;
|
|
1333
1334
|
}
|
|
1334
1335
|
if (message.type === "authResult" && message.ok) {
|
|
1335
|
-
const accepted = { ...message, agent: { protocol: PROTOCOL_VERSION, ...this.options.agentVersion ? { version: this.options.agentVersion } : {} } };
|
|
1336
|
+
const accepted = { ...message, agent: { protocol: PROTOCOL_VERSION, ...this.options.agentVersion ? { version: this.options.agentVersion } : {}, features: [AGENT_FEATURES.lastReply] } };
|
|
1336
1337
|
message = accepted;
|
|
1337
1338
|
if (!this.awaitingPhone) this.cancelDeadline();
|
|
1338
1339
|
this.session.raiseCap(MAX_MESSAGE_BYTES);
|
|
@@ -2550,6 +2551,10 @@ var ConnectionHandler = class {
|
|
|
2550
2551
|
voice.session?.end();
|
|
2551
2552
|
return;
|
|
2552
2553
|
}
|
|
2554
|
+
case "lastReply": {
|
|
2555
|
+
this.conn.send({ type: "lastReply", workspaceId: message.workspaceId, text: this.deps.lastReplies?.get(message.workspaceId) ?? null });
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2553
2558
|
case "ping": {
|
|
2554
2559
|
this.conn.send({ type: "pong" });
|
|
2555
2560
|
return;
|
|
@@ -2941,6 +2946,25 @@ function applyHookEvent(deps, input) {
|
|
|
2941
2946
|
return decision.status ? { workspaceId: input.sessionName, status: decision.status } : null;
|
|
2942
2947
|
}
|
|
2943
2948
|
|
|
2949
|
+
// src/application/last-replies.ts
|
|
2950
|
+
var MAX_REPLY_CHARS = 64e3;
|
|
2951
|
+
var TURN_ENDS = /* @__PURE__ */ new Set(["Stop", "StopFailure"]);
|
|
2952
|
+
var LastReplies = class {
|
|
2953
|
+
replies = /* @__PURE__ */ new Map();
|
|
2954
|
+
record(id, event, payload) {
|
|
2955
|
+
if (!TURN_ENDS.has(event) || typeof payload !== "object" || payload === null) return;
|
|
2956
|
+
const text = payload.last_assistant_message;
|
|
2957
|
+
if (typeof text !== "string" || text.trim() === "") return;
|
|
2958
|
+
this.replies.set(id, text.slice(0, MAX_REPLY_CHARS));
|
|
2959
|
+
}
|
|
2960
|
+
get(id) {
|
|
2961
|
+
return this.replies.get(id) ?? null;
|
|
2962
|
+
}
|
|
2963
|
+
forget(id) {
|
|
2964
|
+
this.replies.delete(id);
|
|
2965
|
+
}
|
|
2966
|
+
};
|
|
2967
|
+
|
|
2944
2968
|
// src/application/send-message.ts
|
|
2945
2969
|
import { randomBytes } from "node:crypto";
|
|
2946
2970
|
function headerSafe(name) {
|
|
@@ -3148,6 +3172,7 @@ var WsServer = class {
|
|
|
3148
3172
|
oscTurnEnded = /* @__PURE__ */ new Set();
|
|
3149
3173
|
// Hooks posted by Codex thread, applied one after another in arrival order.
|
|
3150
3174
|
threadHooks = Promise.resolve();
|
|
3175
|
+
lastReplies = new LastReplies();
|
|
3151
3176
|
// The panes showing a question or menu at the last poll.
|
|
3152
3177
|
asking = /* @__PURE__ */ new Set();
|
|
3153
3178
|
// The last OSC 7501 record seen per pane, so only a change is applied (applyProgramStatus).
|
|
@@ -3178,6 +3203,7 @@ var WsServer = class {
|
|
|
3178
3203
|
this.sentDuringTool.delete(id);
|
|
3179
3204
|
this.oscTurnEnded.delete(id);
|
|
3180
3205
|
this.programSeen.delete(id);
|
|
3206
|
+
this.lastReplies.forget(id);
|
|
3181
3207
|
}
|
|
3182
3208
|
// pollTitles logs and drops a workspace it saw go, but it runs only while a phone is connected
|
|
3183
3209
|
// and never follows a hidden folder. This drops the rest, so an exited workspace's number is
|
|
@@ -3282,7 +3308,8 @@ var WsServer = class {
|
|
|
3282
3308
|
forgetWorkspace: (id, closing) => this.forgetWorkspace(id, closing),
|
|
3283
3309
|
dropWorkspaceState: (id) => this.dropWorkspaceState(id),
|
|
3284
3310
|
broadcastClosed: (id) => this.broadcastClosed(id),
|
|
3285
|
-
messagedLast: this.messagedLast
|
|
3311
|
+
messagedLast: this.messagedLast,
|
|
3312
|
+
lastReplies: this.lastReplies
|
|
3286
3313
|
});
|
|
3287
3314
|
this.handlers.add(handler);
|
|
3288
3315
|
conn.onClose(() => {
|
|
@@ -3306,9 +3333,9 @@ var WsServer = class {
|
|
|
3306
3333
|
res.writeHead(401).end();
|
|
3307
3334
|
return;
|
|
3308
3335
|
}
|
|
3309
|
-
let body = "";
|
|
3310
3336
|
let bytes = 0;
|
|
3311
3337
|
let refused = false;
|
|
3338
|
+
const chunks = [];
|
|
3312
3339
|
req.on("data", (chunk) => {
|
|
3313
3340
|
if (refused) return;
|
|
3314
3341
|
bytes += chunk.length;
|
|
@@ -3318,10 +3345,11 @@ var WsServer = class {
|
|
|
3318
3345
|
req.destroy();
|
|
3319
3346
|
return;
|
|
3320
3347
|
}
|
|
3321
|
-
|
|
3348
|
+
chunks.push(chunk);
|
|
3322
3349
|
});
|
|
3323
3350
|
req.on("end", () => {
|
|
3324
3351
|
if (refused) return;
|
|
3352
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
3325
3353
|
if (route === "/unpark") {
|
|
3326
3354
|
void this.handleUnpark(body, res);
|
|
3327
3355
|
return;
|
|
@@ -3374,6 +3402,7 @@ var WsServer = class {
|
|
|
3374
3402
|
applyHook(sessionName, event, claude, agent) {
|
|
3375
3403
|
const statusBefore = this.deps.status.get(sessionName);
|
|
3376
3404
|
const messagedBefore = this.messagedLast.has(sessionName);
|
|
3405
|
+
this.lastReplies.record(sessionName, event, claude);
|
|
3377
3406
|
applyHookEvent(
|
|
3378
3407
|
{
|
|
3379
3408
|
store: this.deps.status,
|