@tarfa/cli 0.1.6 → 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/dist/{chunk-UPCCR4ZA.js → chunk-4M2ZRGQR.js} +531 -488
- package/dist/cli.js +20 -3
- package/dist/serve.js +1 -1
- package/package.json +1 -1
|
@@ -5151,6 +5151,16 @@ async function loadOrCreateAgentKeys(path) {
|
|
|
5151
5151
|
return parseKeys(await readFile4(path, "utf8"), path);
|
|
5152
5152
|
}
|
|
5153
5153
|
}
|
|
5154
|
+
async function readAgentPublicKey(path) {
|
|
5155
|
+
let text;
|
|
5156
|
+
try {
|
|
5157
|
+
text = await readFile4(path, "utf8");
|
|
5158
|
+
} catch (err) {
|
|
5159
|
+
if (err.code === "ENOENT") throw new Error(`no agent key at ${path}: run tarfa setup first`);
|
|
5160
|
+
throw err;
|
|
5161
|
+
}
|
|
5162
|
+
return parseKeys(text, path).x25519.publicKey;
|
|
5163
|
+
}
|
|
5154
5164
|
function parseKeys(text, path) {
|
|
5155
5165
|
const keyPattern = /^[A-Za-z0-9_-]{43}$/;
|
|
5156
5166
|
const parsed = JSON.parse(text);
|
|
@@ -5276,6 +5286,436 @@ var FileDeviceStore = class {
|
|
|
5276
5286
|
}
|
|
5277
5287
|
};
|
|
5278
5288
|
|
|
5289
|
+
// ../contracts/src/domain/workspace-link.ts
|
|
5290
|
+
var WORKSPACE_LINK_PREFIX = "#open?";
|
|
5291
|
+
var KEY = /^[A-Za-z0-9_-]{43}$/;
|
|
5292
|
+
var ID = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
|
5293
|
+
var valid = (link) => KEY.test(link.agentKey) && ID.test(link.workspaceId);
|
|
5294
|
+
function workspaceLink(pwaOrigin, link) {
|
|
5295
|
+
if (!valid(link)) throw new Error(`not a linkable workspace: ${link.workspaceId}`);
|
|
5296
|
+
return `${new URL(pwaOrigin).origin}/${WORKSPACE_LINK_PREFIX}k=${link.agentKey}&w=${link.workspaceId}`;
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
// ../contracts/src/domain/workspace-status.ts
|
|
5300
|
+
var WORKSPACE_STATUSES = [
|
|
5301
|
+
"needs-feedback",
|
|
5302
|
+
"needs-hands",
|
|
5303
|
+
"idle",
|
|
5304
|
+
"finished",
|
|
5305
|
+
"parked",
|
|
5306
|
+
"blocked",
|
|
5307
|
+
"review",
|
|
5308
|
+
"working"
|
|
5309
|
+
];
|
|
5310
|
+
function isWorkspaceStatus(value) {
|
|
5311
|
+
return typeof value === "string" && WORKSPACE_STATUSES.includes(value);
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
// ../contracts/src/domain/sort-by-status.ts
|
|
5315
|
+
var RANK = Object.fromEntries(WORKSPACE_STATUSES.map((s, i) => [s, i]));
|
|
5316
|
+
|
|
5317
|
+
// ../contracts/src/domain/runs-claude.ts
|
|
5318
|
+
function runsClaude(command) {
|
|
5319
|
+
return /(^|[\s/])claude(\s|$)/.test(command);
|
|
5320
|
+
}
|
|
5321
|
+
|
|
5322
|
+
// ../contracts/src/domain/dictation-key-format.ts
|
|
5323
|
+
var SHAPES = {
|
|
5324
|
+
elevenlabs: [/^sk_[A-Za-z0-9]{32,}$/, /^[0-9a-f]{32}$/],
|
|
5325
|
+
openrouter: [/^sk-or-v\d+-[A-Za-z0-9]{32,}$/]
|
|
5326
|
+
};
|
|
5327
|
+
function dictationKeyProblem(provider, key2) {
|
|
5328
|
+
if (SHAPES[provider].some((shape) => shape.test(key2))) return null;
|
|
5329
|
+
const other = provider === "elevenlabs" ? "openrouter" : "elevenlabs";
|
|
5330
|
+
return SHAPES[other].some((shape) => shape.test(key2)) ? "other-provider" : "format";
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
// ../contracts/src/domain/image-media-type.ts
|
|
5334
|
+
var BY_EXT = {
|
|
5335
|
+
png: "image/png",
|
|
5336
|
+
jpg: "image/jpeg",
|
|
5337
|
+
jpeg: "image/jpeg",
|
|
5338
|
+
gif: "image/gif",
|
|
5339
|
+
webp: "image/webp",
|
|
5340
|
+
avif: "image/avif",
|
|
5341
|
+
bmp: "image/bmp",
|
|
5342
|
+
svg: "image/svg+xml"
|
|
5343
|
+
};
|
|
5344
|
+
function imageMediaType(path) {
|
|
5345
|
+
const dot = path.lastIndexOf(".");
|
|
5346
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
5347
|
+
if (dot <= slash + 1) return "";
|
|
5348
|
+
return BY_EXT[path.slice(dot + 1).toLowerCase()] ?? "";
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
// ../contracts/src/domain/pdf-media-type.ts
|
|
5352
|
+
function pdfMediaType(path) {
|
|
5353
|
+
const dot = path.lastIndexOf(".");
|
|
5354
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
5355
|
+
if (dot <= slash + 1) return "";
|
|
5356
|
+
return path.slice(dot + 1).toLowerCase() === "pdf" ? "application/pdf" : "";
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
// ../contracts/src/domain/audio-media-type.ts
|
|
5360
|
+
var BY_EXT2 = {
|
|
5361
|
+
mp3: "audio/mpeg",
|
|
5362
|
+
m4a: "audio/mp4",
|
|
5363
|
+
aac: "audio/aac",
|
|
5364
|
+
wav: "audio/wav",
|
|
5365
|
+
flac: "audio/flac",
|
|
5366
|
+
ogg: "audio/ogg",
|
|
5367
|
+
oga: "audio/ogg",
|
|
5368
|
+
opus: "audio/ogg"
|
|
5369
|
+
};
|
|
5370
|
+
function audioMediaType(path) {
|
|
5371
|
+
const dot = path.lastIndexOf(".");
|
|
5372
|
+
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
5373
|
+
if (dot <= slash + 1) return "";
|
|
5374
|
+
return BY_EXT2[path.slice(dot + 1).toLowerCase()] ?? "";
|
|
5375
|
+
}
|
|
5376
|
+
|
|
5377
|
+
// ../contracts/src/protocol/messages.ts
|
|
5378
|
+
import { z as z5 } from "zod";
|
|
5379
|
+
|
|
5380
|
+
// ../contracts/src/protocol/session-channels.ts
|
|
5381
|
+
import { z as z4 } from "zod";
|
|
5382
|
+
|
|
5383
|
+
// ../contracts/src/protocol/client-log.ts
|
|
5384
|
+
import { z as z3 } from "zod";
|
|
5385
|
+
var clientLogEventSchema = z3.object({
|
|
5386
|
+
sess: z3.string(),
|
|
5387
|
+
// per-app-launch id
|
|
5388
|
+
seq: z3.number().int().nonnegative(),
|
|
5389
|
+
// monotonic within sess
|
|
5390
|
+
t: z3.number(),
|
|
5391
|
+
// epoch ms at emit
|
|
5392
|
+
machineId: z3.string().optional(),
|
|
5393
|
+
// absent for app-global events (visibility/online)
|
|
5394
|
+
kind: z3.string(),
|
|
5395
|
+
code: z3.number().int().optional(),
|
|
5396
|
+
// WS close code, when reported
|
|
5397
|
+
reason: z3.string().optional(),
|
|
5398
|
+
// classified diagnostic message / close reason
|
|
5399
|
+
online: z3.boolean(),
|
|
5400
|
+
vis: z3.string()
|
|
5401
|
+
});
|
|
5402
|
+
var clientLogBatchSchema = z3.object({
|
|
5403
|
+
ua: z3.string().optional(),
|
|
5404
|
+
shell: z3.string().optional(),
|
|
5405
|
+
events: z3.array(clientLogEventSchema).min(1).max(500)
|
|
5406
|
+
});
|
|
5407
|
+
|
|
5408
|
+
// ../contracts/src/protocol/session-channels.ts
|
|
5409
|
+
var id = z4.string().min(1).max(64);
|
|
5410
|
+
var mediaResultSchema = z4.enum(["ok", "forbidden", "not-found", "too-large", "error"]);
|
|
5411
|
+
var sessionClientMessageSchemas = [
|
|
5412
|
+
z4.object({ type: z4.literal("readMedia"), id, path: z4.string() }),
|
|
5413
|
+
z4.object({ type: z4.literal("mediaAck"), id, seq: z4.number().int().nonnegative() }),
|
|
5414
|
+
z4.object({ type: z4.literal("mediaCancel"), id }),
|
|
5415
|
+
// A browser view: `data` is one message of the browser stream protocol (browser-messages.ts)
|
|
5416
|
+
// as JSON text. Each `browser` the agent sends
|
|
5417
|
+
// is acknowledged with a `browserAck`, which is what lets the agent drop stale frames for a
|
|
5418
|
+
// slow phone (see BrowserStreamHandler).
|
|
5419
|
+
z4.object({ type: z4.literal("browserOpen"), stream: id, session: z4.string() }),
|
|
5420
|
+
z4.object({ type: z4.literal("browser"), stream: id, data: z4.string() }),
|
|
5421
|
+
z4.object({ type: z4.literal("browserAck"), stream: id }),
|
|
5422
|
+
z4.object({ type: z4.literal("browserClose"), stream: id }),
|
|
5423
|
+
z4.object({ type: z4.literal("clientLog"), id, batch: clientLogBatchSchema })
|
|
5424
|
+
];
|
|
5425
|
+
var sessionAgentMessageSchemas = [
|
|
5426
|
+
// The answer to `readMedia`. `size` and `mediaType` come with `ok`; `size` bytes then follow
|
|
5427
|
+
// as chunks numbered from 0.
|
|
5428
|
+
z4.object({
|
|
5429
|
+
type: z4.literal("media"),
|
|
5430
|
+
id,
|
|
5431
|
+
result: mediaResultSchema,
|
|
5432
|
+
mediaType: z4.string().optional(),
|
|
5433
|
+
size: z4.number().int().nonnegative().optional()
|
|
5434
|
+
}),
|
|
5435
|
+
z4.object({ type: z4.literal("mediaChunk"), id, seq: z4.number().int().nonnegative(), data: z4.string() }),
|
|
5436
|
+
z4.object({ type: z4.literal("browser"), stream: id, data: z4.string() }),
|
|
5437
|
+
// The agent ended the stream (the session closed, was taken over, or never existed); the
|
|
5438
|
+
// reason, when there is one, came just before as a browser-protocol `ended`.
|
|
5439
|
+
z4.object({ type: z4.literal("browserClosed"), stream: id }),
|
|
5440
|
+
z4.object({ type: z4.literal("clientLogAck"), id, ok: z4.boolean() })
|
|
5441
|
+
];
|
|
5442
|
+
|
|
5443
|
+
// ../contracts/src/protocol/messages.ts
|
|
5444
|
+
var workspaceStatusSchema = z5.enum(
|
|
5445
|
+
WORKSPACE_STATUSES
|
|
5446
|
+
);
|
|
5447
|
+
var workspaceSchema = z5.object({
|
|
5448
|
+
machineId: z5.string(),
|
|
5449
|
+
id: z5.string(),
|
|
5450
|
+
name: z5.string(),
|
|
5451
|
+
projectPath: z5.string(),
|
|
5452
|
+
command: z5.string(),
|
|
5453
|
+
createdAt: z5.number(),
|
|
5454
|
+
lastActivityAt: z5.number(),
|
|
5455
|
+
status: workspaceStatusSchema,
|
|
5456
|
+
// Optional GitHub owner/repo (parsed from the origin remote by the agent). `.optional()`
|
|
5457
|
+
// keeps a version-skewed rollout safe: an old agent omits it, a new one includes it.
|
|
5458
|
+
github: z5.object({ owner: z5.string(), repo: z5.string(), ownerType: z5.enum(["user", "org"]).optional() }).optional(),
|
|
5459
|
+
// Manual "pin to top of its status group" flag. `.optional()` keeps a version-skewed
|
|
5460
|
+
// rollout safe: an old agent omits it and the PWA reads it as not-urgent.
|
|
5461
|
+
urgent: z5.boolean().optional(),
|
|
5462
|
+
// The name Claude Code's session registry gives the session running in this workspace.
|
|
5463
|
+
// `.optional()` keeps a version-skewed rollout safe: an old agent omits it and the PWA
|
|
5464
|
+
// simply shows no badge. It must be declared here or zod strips it off the wire.
|
|
5465
|
+
agentAddress: z5.string().optional(),
|
|
5466
|
+
parentId: z5.string().optional(),
|
|
5467
|
+
waitingOn: z5.literal("parent").optional(),
|
|
5468
|
+
asking: z5.boolean().optional()
|
|
5469
|
+
});
|
|
5470
|
+
var dim = z5.number().int().positive();
|
|
5471
|
+
var browserSessionSchema = z5.object({ name: z5.string() });
|
|
5472
|
+
var machineStatsSchema = z5.object({
|
|
5473
|
+
cpuPercent: z5.number(),
|
|
5474
|
+
memory: z5.object({ usedBytes: z5.number(), totalBytes: z5.number() }),
|
|
5475
|
+
swapUsedBytes: z5.number(),
|
|
5476
|
+
limit: z5.object({
|
|
5477
|
+
usedBytes: z5.number(),
|
|
5478
|
+
totalBytes: z5.number(),
|
|
5479
|
+
stallPercent: z5.number().optional(),
|
|
5480
|
+
// The kill line. `totalBytes` is min(high, max) — the throttle line the machine is
|
|
5481
|
+
// normally pinned against — so headroom, which measures the distance to an OOM kill,
|
|
5482
|
+
// needs `memory.max` separately. Absent when the cgroup sets no hard limit.
|
|
5483
|
+
maxBytes: z5.number().optional(),
|
|
5484
|
+
// Page cache plus reclaimable slab, less mapped file pages: memory the kernel can
|
|
5485
|
+
// hand back without killing anything. This is what makes `usedBytes` sitting at the
|
|
5486
|
+
// ceiling harmless, and what headroom adds to the distance-to-max.
|
|
5487
|
+
reclaimableBytes: z5.number().optional(),
|
|
5488
|
+
// Cumulative `memory.events` oom_kill for the cgroup. The client keeps the first
|
|
5489
|
+
// value it sees per connection and colours on the delta, so the agent stays dumb.
|
|
5490
|
+
oomKills: z5.number().optional()
|
|
5491
|
+
}).optional()
|
|
5492
|
+
});
|
|
5493
|
+
var dictationProviderSchema = z5.enum(["elevenlabs", "openrouter"]);
|
|
5494
|
+
var clientMessageSchema = z5.discriminatedUnion("type", [
|
|
5495
|
+
// `token` carries a `tarfa pair` code for a new phone, or nothing (an enrolled key needs none).
|
|
5496
|
+
z5.object({ type: z5.literal("auth"), token: z5.string() }),
|
|
5497
|
+
z5.object({ type: z5.literal("list") }),
|
|
5498
|
+
z5.object({ type: z5.literal("getRecentPaths") }),
|
|
5499
|
+
z5.object({ type: z5.literal("browseDir"), path: z5.string() }),
|
|
5500
|
+
// Create a new directory named `name` inside the already-browsed `parent`. The agent
|
|
5501
|
+
// authorizes it against the same allowed-roots set as browseDir, then replies with a
|
|
5502
|
+
// `dirEntries` for the new (empty) folder so the picker browses into it.
|
|
5503
|
+
z5.object({ type: z5.literal("makeDir"), parent: z5.string(), name: z5.string() }),
|
|
5504
|
+
z5.object({ type: z5.literal("listRoots") }),
|
|
5505
|
+
// No longer answered; kept so an agent does not refuse it from an older PWA.
|
|
5506
|
+
z5.object({ type: z5.literal("listCommands") }),
|
|
5507
|
+
z5.object({ type: z5.literal("listLaunchers") }),
|
|
5508
|
+
z5.object({ type: z5.literal("forgetLauncher"), command: z5.string() }),
|
|
5509
|
+
z5.object({ type: z5.literal("listKeyterms") }),
|
|
5510
|
+
// Asked only of an agent whose authResult lists AGENT_FEATURES.dictationKeys.
|
|
5511
|
+
z5.object({ type: z5.literal("getDictationKeys") }),
|
|
5512
|
+
// Store (`key`) or remove (`null`) one of the machine's dictation keys, as `tarfa dictation-key`
|
|
5513
|
+
// does. Sent only to an agent whose authResult lists AGENT_FEATURES.setDictationKey; the key is
|
|
5514
|
+
// checked by the agent, which answers `dictationKeySet` with the same `id`, so a late answer to
|
|
5515
|
+
// an earlier save is never taken for this one's.
|
|
5516
|
+
z5.object({ type: z5.literal("setDictationKey"), id: z5.string(), provider: dictationProviderSchema, key: z5.string().nullable() }),
|
|
5517
|
+
z5.object({ type: z5.literal("readFile"), path: z5.string() }),
|
|
5518
|
+
z5.object({ type: z5.literal("attach"), workspaceId: z5.string(), cols: dim, rows: dim }),
|
|
5519
|
+
z5.object({ type: z5.literal("detach"), workspaceId: z5.string() }),
|
|
5520
|
+
z5.object({ type: z5.literal("input"), workspaceId: z5.string(), data: z5.string() }),
|
|
5521
|
+
z5.object({ type: z5.literal("putFile"), workspaceId: z5.string(), name: z5.string(), data: z5.string() }),
|
|
5522
|
+
// A dictation streamed as it is spoken: 16 kHz mono s16le PCM (base64) the agent plays into
|
|
5523
|
+
// the machine's virtual mic while holding space in the workspace, so Claude Code's /voice
|
|
5524
|
+
// transcribes it. `id` names the dictation, so a late reply to an older one is never taken for
|
|
5525
|
+
// the current one's. Answered with one `voiceDone`; an older agent answers `bad_message`.
|
|
5526
|
+
z5.object({ type: z5.literal("voiceStart"), workspaceId: z5.string(), id: z5.string() }),
|
|
5527
|
+
z5.object({ type: z5.literal("voiceChunk"), id: z5.string(), data: z5.string() }),
|
|
5528
|
+
z5.object({ type: z5.literal("voiceEnd"), id: z5.string() }),
|
|
5529
|
+
// The text of Claude's last reply in a workspace, read aloud on the phone. Sent only to an agent
|
|
5530
|
+
// whose authResult names the `lastReply` feature.
|
|
5531
|
+
z5.object({ type: z5.literal("lastReply"), workspaceId: z5.string() }),
|
|
5532
|
+
z5.object({ type: z5.literal("resize"), workspaceId: z5.string(), cols: dim, rows: dim }),
|
|
5533
|
+
// `remember` is set only when the command came from the Custom field: the agent then adds it
|
|
5534
|
+
// to the machine's launcher list. Additive, so an older agent just ignores it.
|
|
5535
|
+
z5.object({ type: z5.literal("create"), projectPath: z5.string(), command: z5.string(), remember: z5.boolean().optional() }),
|
|
5536
|
+
z5.object({ type: z5.literal("close"), workspaceId: z5.string() }),
|
|
5537
|
+
z5.object({ type: z5.literal("setStatus"), workspaceId: z5.string(), status: workspaceStatusSchema }),
|
|
5538
|
+
// Manually pin/unpin a workspace to the top of its status group. Orthogonal to setStatus;
|
|
5539
|
+
// the agent's onChange broadcasts the refreshed workspace to every device.
|
|
5540
|
+
z5.object({ type: z5.literal("setUrgent"), workspaceId: z5.string(), urgent: z5.boolean() }),
|
|
5541
|
+
// Start an agent-browser session named after the workspace. Old agents answer with their
|
|
5542
|
+
// generic unknown-message error; the PWA only offers this once the agent has proven
|
|
5543
|
+
// support by sending `browserSessions`.
|
|
5544
|
+
z5.object({ type: z5.literal("startBrowser"), workspaceId: z5.string() }),
|
|
5545
|
+
// Liveness probe: the PWA pings periodically so it can detect a half-open socket that
|
|
5546
|
+
// never fires a `close` event (common on mobile) and force a reconnect. The agent answers
|
|
5547
|
+
// with `pong`.
|
|
5548
|
+
z5.object({ type: z5.literal("ping") }),
|
|
5549
|
+
...sessionClientMessageSchemas
|
|
5550
|
+
]);
|
|
5551
|
+
var detachReasonSchema = z5.enum(["opened-elsewhere", "closed", "error"]);
|
|
5552
|
+
var PROTOCOL_VERSION = 1;
|
|
5553
|
+
var AGENT_FEATURES = {
|
|
5554
|
+
lastReply: "lastReply",
|
|
5555
|
+
dictationKeys: "dictationKeys",
|
|
5556
|
+
setDictationKey: "setDictationKey",
|
|
5557
|
+
addresses: "addresses"
|
|
5558
|
+
};
|
|
5559
|
+
var agentMessageSchema = z5.discriminatedUnion("type", [
|
|
5560
|
+
// `credential` is what authenticated the session (`token` only from an agent from before token
|
|
5561
|
+
// enrolment was removed); `enrolled` says the device key is enrolled (written), so the phone
|
|
5562
|
+
// forgets its pairing secret. Both optional: an older agent sends neither.
|
|
5563
|
+
// `pushed`: the agent served the phone at once (its hello asked) and has sent the opening replies
|
|
5564
|
+
// (workspaces, keyterms) behind it, so the phone does not ask for them.
|
|
5565
|
+
z5.object({
|
|
5566
|
+
type: z5.literal("authResult"),
|
|
5567
|
+
ok: z5.boolean(),
|
|
5568
|
+
credential: z5.enum(["device", "pairing", "token"]).optional().catch(void 0),
|
|
5569
|
+
enrolled: z5.boolean().optional().catch(void 0),
|
|
5570
|
+
pushed: z5.literal(true).optional().catch(void 0),
|
|
5571
|
+
// `features` names what this agent answers beyond the protocol, so the PWA offers a control
|
|
5572
|
+
// only to an agent that supports it (a worktree agent shares a release's version number).
|
|
5573
|
+
// `addresses`: where phones reach this agent now (its TARFA_PHONE_ADDRESSES), which a phone
|
|
5574
|
+
// adopts only once it has connected through each one.
|
|
5575
|
+
agent: z5.object({
|
|
5576
|
+
protocol: z5.number().int(),
|
|
5577
|
+
version: z5.string().optional(),
|
|
5578
|
+
features: z5.array(z5.string()).optional().catch(void 0),
|
|
5579
|
+
addresses: z5.array(z5.string()).optional().catch(void 0)
|
|
5580
|
+
}).optional().catch(void 0)
|
|
5581
|
+
}),
|
|
5582
|
+
// `claudeVoice`: whether this machine can play a Claude voice dictation (see voiceStart).
|
|
5583
|
+
// An older agent omits it, and the PWA then treats Claude voice as unavailable there.
|
|
5584
|
+
z5.object({ type: z5.literal("workspaces"), workspaces: z5.array(workspaceSchema), claudeVoice: z5.boolean().optional() }),
|
|
5585
|
+
z5.object({ type: z5.literal("workspaceUpdated"), workspace: workspaceSchema }),
|
|
5586
|
+
z5.object({ type: z5.literal("recentPaths"), paths: z5.array(z5.string()) }),
|
|
5587
|
+
// The agent's configured project roots — the folder picker's top-level browse points.
|
|
5588
|
+
// `roots` is additive: older agents omit it, so it defaults to [] rather than letting the
|
|
5589
|
+
// strict client parser drop the whole message during a version-skewed rollout.
|
|
5590
|
+
z5.object({ type: z5.literal("roots"), roots: z5.array(z5.string()).default([]) }),
|
|
5591
|
+
// What the Create Workspace command list offers on this machine: the known CLIs found
|
|
5592
|
+
// installed (`claude`, `codex`) and the custom commands launched here before. Additive like
|
|
5593
|
+
// `roots`: an older agent never sends it and the PWA keeps its fixed list.
|
|
5594
|
+
z5.object({ type: z5.literal("launchers"), detected: z5.array(z5.string()).default([]), custom: z5.array(z5.string()).default([]) }),
|
|
5595
|
+
// The agent machine's dictation vocabulary (~/.tarfa/keyterms.txt), passed to ElevenLabs as
|
|
5596
|
+
// keyterms so jargon transcribes correctly. Additive like `roots`: older agents omit it,
|
|
5597
|
+
// so it defaults to [] instead of letting the strict client parser drop the whole message.
|
|
5598
|
+
z5.object({ type: z5.literal("keyterms"), keyterms: z5.array(z5.string()).default([]) }),
|
|
5599
|
+
// The dictation provider keys the agent's operator stored (~/.tarfa/dictation-keys.json), sent
|
|
5600
|
+
// only in reply to getDictationKeys. A key the agent does not hold is absent.
|
|
5601
|
+
z5.object({
|
|
5602
|
+
type: z5.literal("dictationKeys"),
|
|
5603
|
+
elevenlabs: z5.string().optional().catch(void 0),
|
|
5604
|
+
openrouter: z5.string().optional().catch(void 0)
|
|
5605
|
+
}),
|
|
5606
|
+
// The answer to setDictationKey. `error` is the agent's fixed wording of a refusal and never
|
|
5607
|
+
// holds the key.
|
|
5608
|
+
z5.object({ type: z5.literal("dictationKeySet"), id: z5.string(), provider: dictationProviderSchema, ok: z5.boolean(), error: z5.string().optional() }),
|
|
5609
|
+
// `files` is additive: older agents that predate it omit the field, so it defaults to []
|
|
5610
|
+
// (a strictly-required field would make the strict client parser silently drop their
|
|
5611
|
+
// dirEntries during a version-skewed rollout, hanging the browser on "Loading…").
|
|
5612
|
+
z5.object({ type: z5.literal("dirEntries"), path: z5.string(), subdirs: z5.array(z5.string()), files: z5.array(z5.string()).default([]) }),
|
|
5613
|
+
// `truncated`/`binary` are additive: older agents omit them, so they default to false (a
|
|
5614
|
+
// strictly-required field would make the strict client parser drop the whole message
|
|
5615
|
+
// during a version-skewed rollout).
|
|
5616
|
+
z5.object({
|
|
5617
|
+
type: z5.literal("fileContents"),
|
|
5618
|
+
path: z5.string(),
|
|
5619
|
+
data: z5.string(),
|
|
5620
|
+
truncated: z5.boolean().default(false),
|
|
5621
|
+
binary: z5.boolean().default(false)
|
|
5622
|
+
}),
|
|
5623
|
+
z5.object({ type: z5.literal("attached"), workspaceId: z5.string() }),
|
|
5624
|
+
z5.object({ type: z5.literal("output"), workspaceId: z5.string(), data: z5.string() }),
|
|
5625
|
+
z5.object({ type: z5.literal("fileStored"), workspaceId: z5.string(), path: z5.string() }),
|
|
5626
|
+
// `text` is null when the agent holds no reply for that workspace (none since it started, or not Claude).
|
|
5627
|
+
z5.object({ type: z5.literal("lastReply"), workspaceId: z5.string(), text: z5.string().nullable() }),
|
|
5628
|
+
z5.object({ type: z5.literal("voiceDone"), id: z5.string(), ok: z5.boolean(), error: z5.string().optional() }),
|
|
5629
|
+
z5.object({ type: z5.literal("detached"), workspaceId: z5.string(), reason: detachReasonSchema }),
|
|
5630
|
+
z5.object({ type: z5.literal("closed"), workspaceId: z5.string() }),
|
|
5631
|
+
// A phone was just enrolled on this machine (a pairing code), told to every other
|
|
5632
|
+
// phone connected to it: a device nobody meant to add must not go unnoticed. An older PWA drops
|
|
5633
|
+
// the unknown type.
|
|
5634
|
+
z5.object({ type: z5.literal("deviceEnrolled"), id: z5.string(), name: z5.string(), at: z5.string() }),
|
|
5635
|
+
z5.object({ type: z5.literal("error"), code: z5.string(), message: z5.string() }),
|
|
5636
|
+
// Live agent-browser sessions, sent after every `workspaces` reply. `sessions` defaults
|
|
5637
|
+
// to [] so a skewed sender omitting it can't make the strict client parser drop the
|
|
5638
|
+
// whole message.
|
|
5639
|
+
z5.object({ type: z5.literal("browserSessions"), sessions: z5.array(browserSessionSchema).default([]) }),
|
|
5640
|
+
// The agent machine's current CPU/memory load (see machineStatsSchema). Pushed on a
|
|
5641
|
+
// timer while at least one client is connected; an agent that predates it simply never
|
|
5642
|
+
// sends one and the PWA shows no load strip for that machine.
|
|
5643
|
+
z5.object({ type: z5.literal("stats"), stats: machineStatsSchema }),
|
|
5644
|
+
// Reply to the client's liveness `ping` (see clientMessageSchema).
|
|
5645
|
+
z5.object({ type: z5.literal("pong") }),
|
|
5646
|
+
...sessionAgentMessageSchemas
|
|
5647
|
+
]);
|
|
5648
|
+
function parseClientMessage(raw) {
|
|
5649
|
+
return clientMessageSchema.parse(raw);
|
|
5650
|
+
}
|
|
5651
|
+
|
|
5652
|
+
// ../contracts/src/protocol/browser-messages.ts
|
|
5653
|
+
import { z as z6 } from "zod";
|
|
5654
|
+
var modifiers = z6.number().int().optional();
|
|
5655
|
+
var browserClientMessageSchema = z6.discriminatedUnion("type", [
|
|
5656
|
+
z6.object({ type: z6.literal("auth"), token: z6.string() }),
|
|
5657
|
+
// Liveness probe, same pattern as the main terminal socket.
|
|
5658
|
+
z6.object({ type: z6.literal("ping") }),
|
|
5659
|
+
z6.object({
|
|
5660
|
+
type: z6.literal("input_mouse"),
|
|
5661
|
+
eventType: z6.string(),
|
|
5662
|
+
x: z6.number(),
|
|
5663
|
+
y: z6.number(),
|
|
5664
|
+
button: z6.string().optional(),
|
|
5665
|
+
clickCount: z6.number().int().optional(),
|
|
5666
|
+
deltaX: z6.number().optional(),
|
|
5667
|
+
deltaY: z6.number().optional(),
|
|
5668
|
+
modifiers
|
|
5669
|
+
}),
|
|
5670
|
+
z6.object({
|
|
5671
|
+
type: z6.literal("input_keyboard"),
|
|
5672
|
+
eventType: z6.string(),
|
|
5673
|
+
key: z6.string().optional(),
|
|
5674
|
+
code: z6.string().optional(),
|
|
5675
|
+
text: z6.string().optional(),
|
|
5676
|
+
windowsVirtualKeyCode: z6.number().int().optional(),
|
|
5677
|
+
modifiers
|
|
5678
|
+
}),
|
|
5679
|
+
z6.object({
|
|
5680
|
+
type: z6.literal("input_touch"),
|
|
5681
|
+
eventType: z6.string(),
|
|
5682
|
+
touchPoints: z6.array(z6.object({ x: z6.number(), y: z6.number() }).passthrough()),
|
|
5683
|
+
modifiers
|
|
5684
|
+
}),
|
|
5685
|
+
// Handled by the agent (CLI navigation/session control), never forwarded upstream.
|
|
5686
|
+
z6.object({ type: z6.literal("navigate"), url: z6.string() }),
|
|
5687
|
+
z6.object({ type: z6.literal("back") }),
|
|
5688
|
+
z6.object({ type: z6.literal("forward") }),
|
|
5689
|
+
z6.object({ type: z6.literal("closeSession") }),
|
|
5690
|
+
// Resize the remote browser viewport to the attached viewer's canvas (CSS pixels).
|
|
5691
|
+
z6.object({
|
|
5692
|
+
type: z6.literal("setViewport"),
|
|
5693
|
+
width: z6.number().int().positive(),
|
|
5694
|
+
height: z6.number().int().positive()
|
|
5695
|
+
})
|
|
5696
|
+
]);
|
|
5697
|
+
var browserEndedReasonSchema = z6.enum(["closed", "not-found", "error", "opened-elsewhere"]);
|
|
5698
|
+
var browserAgentMessageSchema = z6.discriminatedUnion("type", [
|
|
5699
|
+
z6.object({ type: z6.literal("authResult"), ok: z6.boolean() }),
|
|
5700
|
+
z6.object({ type: z6.literal("pong") }),
|
|
5701
|
+
z6.object({ type: z6.literal("ended"), reason: browserEndedReasonSchema }),
|
|
5702
|
+
z6.object({
|
|
5703
|
+
type: z6.literal("frame"),
|
|
5704
|
+
data: z6.string(),
|
|
5705
|
+
// base64 JPEG
|
|
5706
|
+
metadata: z6.object({ deviceWidth: z6.number(), deviceHeight: z6.number() }).passthrough()
|
|
5707
|
+
}).passthrough(),
|
|
5708
|
+
z6.object({ type: z6.literal("tabs") }).passthrough(),
|
|
5709
|
+
z6.object({ type: z6.literal("url"), url: z6.string() }).passthrough(),
|
|
5710
|
+
z6.object({ type: z6.literal("status") }).passthrough()
|
|
5711
|
+
]);
|
|
5712
|
+
function parseBrowserClientMessage(raw) {
|
|
5713
|
+
return browserClientMessageSchema.parse(raw);
|
|
5714
|
+
}
|
|
5715
|
+
function parseBrowserAgentMessage(raw) {
|
|
5716
|
+
return browserAgentMessageSchema.parse(raw);
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5279
5719
|
// src/application/dictation-keys.ts
|
|
5280
5720
|
var DICTATION_PROVIDERS = ["elevenlabs", "openrouter"];
|
|
5281
5721
|
var MAX_KEY_LENGTH = 512;
|
|
@@ -5289,6 +5729,16 @@ function checkKey(raw) {
|
|
|
5289
5729
|
if (key2.length > MAX_KEY_LENGTH) return { ok: false, error: `a key is at most ${MAX_KEY_LENGTH} characters` };
|
|
5290
5730
|
return { ok: true, key: key2 };
|
|
5291
5731
|
}
|
|
5732
|
+
var SHAPE = {
|
|
5733
|
+
elevenlabs: "an ElevenLabs key starts with sk_ followed by at least 32 letters and digits",
|
|
5734
|
+
openrouter: "an OpenRouter key starts with sk-or-v1- followed by at least 32 letters and digits"
|
|
5735
|
+
};
|
|
5736
|
+
var OTHER = { elevenlabs: "OpenRouter", openrouter: "ElevenLabs" };
|
|
5737
|
+
function keyFormatError(provider, key2) {
|
|
5738
|
+
const problem = dictationKeyProblem(provider, key2);
|
|
5739
|
+
if (problem === null) return null;
|
|
5740
|
+
return problem === "other-provider" ? `this looks like an ${OTHER[provider]} key; ${SHAPE[provider]}` : SHAPE[provider];
|
|
5741
|
+
}
|
|
5292
5742
|
function parseDictationKeys(raw) {
|
|
5293
5743
|
let value;
|
|
5294
5744
|
try {
|
|
@@ -5318,6 +5768,8 @@ async function changeDictationKey(store, provider, raw) {
|
|
|
5318
5768
|
if (raw !== null) {
|
|
5319
5769
|
const checked = checkKey(raw);
|
|
5320
5770
|
if (!checked.ok) return checked;
|
|
5771
|
+
const formatError = keyFormatError(provider, checked.key);
|
|
5772
|
+
if (formatError) return { ok: false, error: formatError };
|
|
5321
5773
|
key2 = checked.key;
|
|
5322
5774
|
}
|
|
5323
5775
|
const result = await store.update((keys) => key2 === null ? withoutKey(keys, provider) : withKey(keys, provider, key2));
|
|
@@ -5409,8 +5861,8 @@ function pairingName(machineId, hostname) {
|
|
|
5409
5861
|
return GENERIC_MACHINE_IDS.has(machineId) ? hostname.split(".")[0] : machineId;
|
|
5410
5862
|
}
|
|
5411
5863
|
function announcedAddresses(phoneAddresses2) {
|
|
5412
|
-
const
|
|
5413
|
-
return
|
|
5864
|
+
const valid2 = phoneAddresses2.length > 0 && phoneAddresses2.length <= MAX_ADDRESSES && phoneAddresses2.every(isMachineUrl);
|
|
5865
|
+
return valid2 ? phoneAddresses2 : void 0;
|
|
5414
5866
|
}
|
|
5415
5867
|
async function mintPairing(deps, input) {
|
|
5416
5868
|
if (deps.defaultPath !== void 0 && !isPairingPath(deps.defaultPath)) {
|
|
@@ -5429,8 +5881,8 @@ async function mintPairing(deps, input) {
|
|
|
5429
5881
|
|
|
5430
5882
|
// src/infrastructure/file-pairing-store.ts
|
|
5431
5883
|
import { readFile as readFile7 } from "node:fs/promises";
|
|
5432
|
-
import { z as
|
|
5433
|
-
var schema =
|
|
5884
|
+
import { z as z7 } from "zod";
|
|
5885
|
+
var schema = z7.array(z7.object({ secret: z7.string().regex(/^[A-Za-z0-9_-]{43}$/), expiresAt: z7.string(), usedAt: z7.string().optional(), usedBy: z7.string().optional() }));
|
|
5434
5886
|
var FilePairingStore = class {
|
|
5435
5887
|
constructor(filePath, staleLockMs = 1e4) {
|
|
5436
5888
|
this.filePath = filePath;
|
|
@@ -5453,489 +5905,78 @@ var FilePairingStore = class {
|
|
|
5453
5905
|
if (JSON.stringify(next) !== JSON.stringify(current)) await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
|
|
5454
5906
|
`);
|
|
5455
5907
|
return next;
|
|
5456
|
-
});
|
|
5457
|
-
}
|
|
5458
|
-
};
|
|
5459
|
-
|
|
5460
|
-
// src/infrastructure/agent-env-file.ts
|
|
5461
|
-
import { execFile as execFile3 } from "node:child_process";
|
|
5462
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
5463
|
-
import { chmod as chmod2, copyFile, mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
|
|
5464
|
-
import { dirname as dirname8, resolve as resolve5 } from "node:path";
|
|
5465
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5466
|
-
import { promisify as promisify3 } from "node:util";
|
|
5467
|
-
|
|
5468
|
-
// src/application/agent-env-location.ts
|
|
5469
|
-
import { join as join6 } from "node:path";
|
|
5470
|
-
function resolveAgentEnvPath(input) {
|
|
5471
|
-
if (input.env.TARFA_AGENT_ENV) return input.env.TARFA_AGENT_ENV;
|
|
5472
|
-
const stateFile = join6(input.home, ".tarfa", "agent.env");
|
|
5473
|
-
if (input.exists(stateFile) || input.checkout === null) return stateFile;
|
|
5474
|
-
return join6(input.checkout, "deploy", "agent.env");
|
|
5475
|
-
}
|
|
5476
|
-
|
|
5477
|
-
// src/infrastructure/agent-env-file.ts
|
|
5478
|
-
var exec3 = promisify3(execFile3);
|
|
5479
|
-
function withoutGitVars(env) {
|
|
5480
|
-
return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith("GIT_")));
|
|
5481
|
-
}
|
|
5482
|
-
async function mainCheckout(kind2 = installKind()) {
|
|
5483
|
-
if (kind2 === "package") return null;
|
|
5484
|
-
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
5485
|
-
try {
|
|
5486
|
-
const { stdout } = await exec3("git", ["-C", here, "rev-parse", "--path-format=absolute", "--git-common-dir"], { env: withoutGitVars(process.env) });
|
|
5487
|
-
return dirname8(stdout.trim());
|
|
5488
|
-
} catch {
|
|
5489
|
-
return resolve5(here, "..", "..", "..", "..");
|
|
5490
|
-
}
|
|
5491
|
-
}
|
|
5492
|
-
async function locateAgentEnv(env, home, kind2 = installKind()) {
|
|
5493
|
-
return resolveAgentEnvPath({ env, home, checkout: await mainCheckout(kind2), exists: existsSync3 });
|
|
5494
|
-
}
|
|
5495
|
-
function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
5496
|
-
return {
|
|
5497
|
-
path,
|
|
5498
|
-
read: async () => {
|
|
5499
|
-
try {
|
|
5500
|
-
return await readFile8(path, "utf8");
|
|
5501
|
-
} catch (err) {
|
|
5502
|
-
if (err.code === "ENOENT") {
|
|
5503
|
-
throw new Error(`no agent config at ${path}: set the agent up first (docs/DEPLOY.md), or set TARFA_AGENT_ENV`);
|
|
5504
|
-
}
|
|
5505
|
-
throw err;
|
|
5506
|
-
}
|
|
5507
|
-
},
|
|
5508
|
-
write: async (text) => {
|
|
5509
|
-
let backup = `${path}.bak-${now().toISOString().replace(/[:.]/g, "-")}`;
|
|
5510
|
-
try {
|
|
5511
|
-
await copyFile(path, backup);
|
|
5512
|
-
await chmod2(backup, 384);
|
|
5513
|
-
} catch (err) {
|
|
5514
|
-
if (err.code !== "ENOENT") throw err;
|
|
5515
|
-
backup = null;
|
|
5516
|
-
}
|
|
5517
|
-
await mkdir7(dirname8(path), { recursive: true, mode: 448 });
|
|
5518
|
-
await writeFile5(path, text, { mode: 384 });
|
|
5519
|
-
await chmod2(path, 384);
|
|
5520
|
-
return backup;
|
|
5521
|
-
}
|
|
5522
|
-
};
|
|
5523
|
-
}
|
|
5524
|
-
|
|
5525
|
-
// src/infrastructure/file-status-store.ts
|
|
5526
|
-
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
5527
|
-
import { dirname as dirname9 } from "node:path";
|
|
5528
|
-
|
|
5529
|
-
// ../contracts/src/domain/workspace-status.ts
|
|
5530
|
-
var WORKSPACE_STATUSES = [
|
|
5531
|
-
"needs-feedback",
|
|
5532
|
-
"needs-hands",
|
|
5533
|
-
"idle",
|
|
5534
|
-
"finished",
|
|
5535
|
-
"parked",
|
|
5536
|
-
"blocked",
|
|
5537
|
-
"review",
|
|
5538
|
-
"working"
|
|
5539
|
-
];
|
|
5540
|
-
function isWorkspaceStatus(value) {
|
|
5541
|
-
return typeof value === "string" && WORKSPACE_STATUSES.includes(value);
|
|
5542
|
-
}
|
|
5543
|
-
|
|
5544
|
-
// ../contracts/src/domain/sort-by-status.ts
|
|
5545
|
-
var RANK = Object.fromEntries(WORKSPACE_STATUSES.map((s, i) => [s, i]));
|
|
5546
|
-
|
|
5547
|
-
// ../contracts/src/domain/runs-claude.ts
|
|
5548
|
-
function runsClaude(command) {
|
|
5549
|
-
return /(^|[\s/])claude(\s|$)/.test(command);
|
|
5550
|
-
}
|
|
5551
|
-
|
|
5552
|
-
// ../contracts/src/domain/image-media-type.ts
|
|
5553
|
-
var BY_EXT = {
|
|
5554
|
-
png: "image/png",
|
|
5555
|
-
jpg: "image/jpeg",
|
|
5556
|
-
jpeg: "image/jpeg",
|
|
5557
|
-
gif: "image/gif",
|
|
5558
|
-
webp: "image/webp",
|
|
5559
|
-
avif: "image/avif",
|
|
5560
|
-
bmp: "image/bmp",
|
|
5561
|
-
svg: "image/svg+xml"
|
|
5562
|
-
};
|
|
5563
|
-
function imageMediaType(path) {
|
|
5564
|
-
const dot = path.lastIndexOf(".");
|
|
5565
|
-
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
5566
|
-
if (dot <= slash + 1) return "";
|
|
5567
|
-
return BY_EXT[path.slice(dot + 1).toLowerCase()] ?? "";
|
|
5568
|
-
}
|
|
5569
|
-
|
|
5570
|
-
// ../contracts/src/domain/pdf-media-type.ts
|
|
5571
|
-
function pdfMediaType(path) {
|
|
5572
|
-
const dot = path.lastIndexOf(".");
|
|
5573
|
-
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
5574
|
-
if (dot <= slash + 1) return "";
|
|
5575
|
-
return path.slice(dot + 1).toLowerCase() === "pdf" ? "application/pdf" : "";
|
|
5576
|
-
}
|
|
5577
|
-
|
|
5578
|
-
// ../contracts/src/domain/audio-media-type.ts
|
|
5579
|
-
var BY_EXT2 = {
|
|
5580
|
-
mp3: "audio/mpeg",
|
|
5581
|
-
m4a: "audio/mp4",
|
|
5582
|
-
aac: "audio/aac",
|
|
5583
|
-
wav: "audio/wav",
|
|
5584
|
-
flac: "audio/flac",
|
|
5585
|
-
ogg: "audio/ogg",
|
|
5586
|
-
oga: "audio/ogg",
|
|
5587
|
-
opus: "audio/ogg"
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5588
5910
|
};
|
|
5589
|
-
function audioMediaType(path) {
|
|
5590
|
-
const dot = path.lastIndexOf(".");
|
|
5591
|
-
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
5592
|
-
if (dot <= slash + 1) return "";
|
|
5593
|
-
return BY_EXT2[path.slice(dot + 1).toLowerCase()] ?? "";
|
|
5594
|
-
}
|
|
5595
|
-
|
|
5596
|
-
// ../contracts/src/protocol/messages.ts
|
|
5597
|
-
import { z as z6 } from "zod";
|
|
5598
|
-
|
|
5599
|
-
// ../contracts/src/protocol/session-channels.ts
|
|
5600
|
-
import { z as z5 } from "zod";
|
|
5601
|
-
|
|
5602
|
-
// ../contracts/src/protocol/client-log.ts
|
|
5603
|
-
import { z as z4 } from "zod";
|
|
5604
|
-
var clientLogEventSchema = z4.object({
|
|
5605
|
-
sess: z4.string(),
|
|
5606
|
-
// per-app-launch id
|
|
5607
|
-
seq: z4.number().int().nonnegative(),
|
|
5608
|
-
// monotonic within sess
|
|
5609
|
-
t: z4.number(),
|
|
5610
|
-
// epoch ms at emit
|
|
5611
|
-
machineId: z4.string().optional(),
|
|
5612
|
-
// absent for app-global events (visibility/online)
|
|
5613
|
-
kind: z4.string(),
|
|
5614
|
-
code: z4.number().int().optional(),
|
|
5615
|
-
// WS close code, when reported
|
|
5616
|
-
reason: z4.string().optional(),
|
|
5617
|
-
// classified diagnostic message / close reason
|
|
5618
|
-
online: z4.boolean(),
|
|
5619
|
-
vis: z4.string()
|
|
5620
|
-
});
|
|
5621
|
-
var clientLogBatchSchema = z4.object({
|
|
5622
|
-
ua: z4.string().optional(),
|
|
5623
|
-
shell: z4.string().optional(),
|
|
5624
|
-
events: z4.array(clientLogEventSchema).min(1).max(500)
|
|
5625
|
-
});
|
|
5626
5911
|
|
|
5627
|
-
//
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
// A browser view: `data` is one message of the browser stream protocol (browser-messages.ts)
|
|
5635
|
-
// as JSON text. Each `browser` the agent sends
|
|
5636
|
-
// is acknowledged with a `browserAck`, which is what lets the agent drop stale frames for a
|
|
5637
|
-
// slow phone (see BrowserStreamHandler).
|
|
5638
|
-
z5.object({ type: z5.literal("browserOpen"), stream: id, session: z5.string() }),
|
|
5639
|
-
z5.object({ type: z5.literal("browser"), stream: id, data: z5.string() }),
|
|
5640
|
-
z5.object({ type: z5.literal("browserAck"), stream: id }),
|
|
5641
|
-
z5.object({ type: z5.literal("browserClose"), stream: id }),
|
|
5642
|
-
z5.object({ type: z5.literal("clientLog"), id, batch: clientLogBatchSchema })
|
|
5643
|
-
];
|
|
5644
|
-
var sessionAgentMessageSchemas = [
|
|
5645
|
-
// The answer to `readMedia`. `size` and `mediaType` come with `ok`; `size` bytes then follow
|
|
5646
|
-
// as chunks numbered from 0.
|
|
5647
|
-
z5.object({
|
|
5648
|
-
type: z5.literal("media"),
|
|
5649
|
-
id,
|
|
5650
|
-
result: mediaResultSchema,
|
|
5651
|
-
mediaType: z5.string().optional(),
|
|
5652
|
-
size: z5.number().int().nonnegative().optional()
|
|
5653
|
-
}),
|
|
5654
|
-
z5.object({ type: z5.literal("mediaChunk"), id, seq: z5.number().int().nonnegative(), data: z5.string() }),
|
|
5655
|
-
z5.object({ type: z5.literal("browser"), stream: id, data: z5.string() }),
|
|
5656
|
-
// The agent ended the stream (the session closed, was taken over, or never existed); the
|
|
5657
|
-
// reason, when there is one, came just before as a browser-protocol `ended`.
|
|
5658
|
-
z5.object({ type: z5.literal("browserClosed"), stream: id }),
|
|
5659
|
-
z5.object({ type: z5.literal("clientLogAck"), id, ok: z5.boolean() })
|
|
5660
|
-
];
|
|
5912
|
+
// src/infrastructure/agent-env-file.ts
|
|
5913
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
5914
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5915
|
+
import { chmod as chmod2, copyFile, mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
|
|
5916
|
+
import { dirname as dirname8, resolve as resolve5 } from "node:path";
|
|
5917
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5918
|
+
import { promisify as promisify3 } from "node:util";
|
|
5661
5919
|
|
|
5662
|
-
//
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
);
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
name: z6.string(),
|
|
5670
|
-
projectPath: z6.string(),
|
|
5671
|
-
command: z6.string(),
|
|
5672
|
-
createdAt: z6.number(),
|
|
5673
|
-
lastActivityAt: z6.number(),
|
|
5674
|
-
status: workspaceStatusSchema,
|
|
5675
|
-
// Optional GitHub owner/repo (parsed from the origin remote by the agent). `.optional()`
|
|
5676
|
-
// keeps a version-skewed rollout safe: an old agent omits it, a new one includes it.
|
|
5677
|
-
github: z6.object({ owner: z6.string(), repo: z6.string(), ownerType: z6.enum(["user", "org"]).optional() }).optional(),
|
|
5678
|
-
// Manual "pin to top of its status group" flag. `.optional()` keeps a version-skewed
|
|
5679
|
-
// rollout safe: an old agent omits it and the PWA reads it as not-urgent.
|
|
5680
|
-
urgent: z6.boolean().optional(),
|
|
5681
|
-
// The name Claude Code's session registry gives the session running in this workspace.
|
|
5682
|
-
// `.optional()` keeps a version-skewed rollout safe: an old agent omits it and the PWA
|
|
5683
|
-
// simply shows no badge. It must be declared here or zod strips it off the wire.
|
|
5684
|
-
agentAddress: z6.string().optional(),
|
|
5685
|
-
parentId: z6.string().optional(),
|
|
5686
|
-
waitingOn: z6.literal("parent").optional(),
|
|
5687
|
-
asking: z6.boolean().optional()
|
|
5688
|
-
});
|
|
5689
|
-
var dim = z6.number().int().positive();
|
|
5690
|
-
var browserSessionSchema = z6.object({ name: z6.string() });
|
|
5691
|
-
var machineStatsSchema = z6.object({
|
|
5692
|
-
cpuPercent: z6.number(),
|
|
5693
|
-
memory: z6.object({ usedBytes: z6.number(), totalBytes: z6.number() }),
|
|
5694
|
-
swapUsedBytes: z6.number(),
|
|
5695
|
-
limit: z6.object({
|
|
5696
|
-
usedBytes: z6.number(),
|
|
5697
|
-
totalBytes: z6.number(),
|
|
5698
|
-
stallPercent: z6.number().optional(),
|
|
5699
|
-
// The kill line. `totalBytes` is min(high, max) — the throttle line the machine is
|
|
5700
|
-
// normally pinned against — so headroom, which measures the distance to an OOM kill,
|
|
5701
|
-
// needs `memory.max` separately. Absent when the cgroup sets no hard limit.
|
|
5702
|
-
maxBytes: z6.number().optional(),
|
|
5703
|
-
// Page cache plus reclaimable slab, less mapped file pages: memory the kernel can
|
|
5704
|
-
// hand back without killing anything. This is what makes `usedBytes` sitting at the
|
|
5705
|
-
// ceiling harmless, and what headroom adds to the distance-to-max.
|
|
5706
|
-
reclaimableBytes: z6.number().optional(),
|
|
5707
|
-
// Cumulative `memory.events` oom_kill for the cgroup. The client keeps the first
|
|
5708
|
-
// value it sees per connection and colours on the delta, so the agent stays dumb.
|
|
5709
|
-
oomKills: z6.number().optional()
|
|
5710
|
-
}).optional()
|
|
5711
|
-
});
|
|
5712
|
-
var dictationProviderSchema = z6.enum(["elevenlabs", "openrouter"]);
|
|
5713
|
-
var clientMessageSchema = z6.discriminatedUnion("type", [
|
|
5714
|
-
// `token` carries a `tarfa pair` code for a new phone, or nothing (an enrolled key needs none).
|
|
5715
|
-
z6.object({ type: z6.literal("auth"), token: z6.string() }),
|
|
5716
|
-
z6.object({ type: z6.literal("list") }),
|
|
5717
|
-
z6.object({ type: z6.literal("getRecentPaths") }),
|
|
5718
|
-
z6.object({ type: z6.literal("browseDir"), path: z6.string() }),
|
|
5719
|
-
// Create a new directory named `name` inside the already-browsed `parent`. The agent
|
|
5720
|
-
// authorizes it against the same allowed-roots set as browseDir, then replies with a
|
|
5721
|
-
// `dirEntries` for the new (empty) folder so the picker browses into it.
|
|
5722
|
-
z6.object({ type: z6.literal("makeDir"), parent: z6.string(), name: z6.string() }),
|
|
5723
|
-
z6.object({ type: z6.literal("listRoots") }),
|
|
5724
|
-
// No longer answered; kept so an agent does not refuse it from an older PWA.
|
|
5725
|
-
z6.object({ type: z6.literal("listCommands") }),
|
|
5726
|
-
z6.object({ type: z6.literal("listLaunchers") }),
|
|
5727
|
-
z6.object({ type: z6.literal("forgetLauncher"), command: z6.string() }),
|
|
5728
|
-
z6.object({ type: z6.literal("listKeyterms") }),
|
|
5729
|
-
// Asked only of an agent whose authResult lists AGENT_FEATURES.dictationKeys.
|
|
5730
|
-
z6.object({ type: z6.literal("getDictationKeys") }),
|
|
5731
|
-
// Store (`key`) or remove (`null`) one of the machine's dictation keys, as `tarfa dictation-key`
|
|
5732
|
-
// does. Sent only to an agent whose authResult lists AGENT_FEATURES.setDictationKey; the key is
|
|
5733
|
-
// checked by the agent, which answers `dictationKeySet` with the same `id`, so a late answer to
|
|
5734
|
-
// an earlier save is never taken for this one's.
|
|
5735
|
-
z6.object({ type: z6.literal("setDictationKey"), id: z6.string(), provider: dictationProviderSchema, key: z6.string().nullable() }),
|
|
5736
|
-
z6.object({ type: z6.literal("readFile"), path: z6.string() }),
|
|
5737
|
-
z6.object({ type: z6.literal("attach"), workspaceId: z6.string(), cols: dim, rows: dim }),
|
|
5738
|
-
z6.object({ type: z6.literal("detach"), workspaceId: z6.string() }),
|
|
5739
|
-
z6.object({ type: z6.literal("input"), workspaceId: z6.string(), data: z6.string() }),
|
|
5740
|
-
z6.object({ type: z6.literal("putFile"), workspaceId: z6.string(), name: z6.string(), data: z6.string() }),
|
|
5741
|
-
// A dictation streamed as it is spoken: 16 kHz mono s16le PCM (base64) the agent plays into
|
|
5742
|
-
// the machine's virtual mic while holding space in the workspace, so Claude Code's /voice
|
|
5743
|
-
// transcribes it. `id` names the dictation, so a late reply to an older one is never taken for
|
|
5744
|
-
// the current one's. Answered with one `voiceDone`; an older agent answers `bad_message`.
|
|
5745
|
-
z6.object({ type: z6.literal("voiceStart"), workspaceId: z6.string(), id: z6.string() }),
|
|
5746
|
-
z6.object({ type: z6.literal("voiceChunk"), id: z6.string(), data: z6.string() }),
|
|
5747
|
-
z6.object({ type: z6.literal("voiceEnd"), id: z6.string() }),
|
|
5748
|
-
// The text of Claude's last reply in a workspace, read aloud on the phone. Sent only to an agent
|
|
5749
|
-
// whose authResult names the `lastReply` feature.
|
|
5750
|
-
z6.object({ type: z6.literal("lastReply"), workspaceId: z6.string() }),
|
|
5751
|
-
z6.object({ type: z6.literal("resize"), workspaceId: z6.string(), cols: dim, rows: dim }),
|
|
5752
|
-
// `remember` is set only when the command came from the Custom field: the agent then adds it
|
|
5753
|
-
// to the machine's launcher list. Additive, so an older agent just ignores it.
|
|
5754
|
-
z6.object({ type: z6.literal("create"), projectPath: z6.string(), command: z6.string(), remember: z6.boolean().optional() }),
|
|
5755
|
-
z6.object({ type: z6.literal("close"), workspaceId: z6.string() }),
|
|
5756
|
-
z6.object({ type: z6.literal("setStatus"), workspaceId: z6.string(), status: workspaceStatusSchema }),
|
|
5757
|
-
// Manually pin/unpin a workspace to the top of its status group. Orthogonal to setStatus;
|
|
5758
|
-
// the agent's onChange broadcasts the refreshed workspace to every device.
|
|
5759
|
-
z6.object({ type: z6.literal("setUrgent"), workspaceId: z6.string(), urgent: z6.boolean() }),
|
|
5760
|
-
// Start an agent-browser session named after the workspace. Old agents answer with their
|
|
5761
|
-
// generic unknown-message error; the PWA only offers this once the agent has proven
|
|
5762
|
-
// support by sending `browserSessions`.
|
|
5763
|
-
z6.object({ type: z6.literal("startBrowser"), workspaceId: z6.string() }),
|
|
5764
|
-
// Liveness probe: the PWA pings periodically so it can detect a half-open socket that
|
|
5765
|
-
// never fires a `close` event (common on mobile) and force a reconnect. The agent answers
|
|
5766
|
-
// with `pong`.
|
|
5767
|
-
z6.object({ type: z6.literal("ping") }),
|
|
5768
|
-
...sessionClientMessageSchemas
|
|
5769
|
-
]);
|
|
5770
|
-
var detachReasonSchema = z6.enum(["opened-elsewhere", "closed", "error"]);
|
|
5771
|
-
var PROTOCOL_VERSION = 1;
|
|
5772
|
-
var AGENT_FEATURES = {
|
|
5773
|
-
lastReply: "lastReply",
|
|
5774
|
-
dictationKeys: "dictationKeys",
|
|
5775
|
-
setDictationKey: "setDictationKey",
|
|
5776
|
-
addresses: "addresses"
|
|
5777
|
-
};
|
|
5778
|
-
var agentMessageSchema = z6.discriminatedUnion("type", [
|
|
5779
|
-
// `credential` is what authenticated the session (`token` only from an agent from before token
|
|
5780
|
-
// enrolment was removed); `enrolled` says the device key is enrolled (written), so the phone
|
|
5781
|
-
// forgets its pairing secret. Both optional: an older agent sends neither.
|
|
5782
|
-
// `pushed`: the agent served the phone at once (its hello asked) and has sent the opening replies
|
|
5783
|
-
// (workspaces, keyterms) behind it, so the phone does not ask for them.
|
|
5784
|
-
z6.object({
|
|
5785
|
-
type: z6.literal("authResult"),
|
|
5786
|
-
ok: z6.boolean(),
|
|
5787
|
-
credential: z6.enum(["device", "pairing", "token"]).optional().catch(void 0),
|
|
5788
|
-
enrolled: z6.boolean().optional().catch(void 0),
|
|
5789
|
-
pushed: z6.literal(true).optional().catch(void 0),
|
|
5790
|
-
// `features` names what this agent answers beyond the protocol, so the PWA offers a control
|
|
5791
|
-
// only to an agent that supports it (a worktree agent shares a release's version number).
|
|
5792
|
-
// `addresses`: where phones reach this agent now (its TARFA_PHONE_ADDRESSES), which a phone
|
|
5793
|
-
// adopts only once it has connected through each one.
|
|
5794
|
-
agent: z6.object({
|
|
5795
|
-
protocol: z6.number().int(),
|
|
5796
|
-
version: z6.string().optional(),
|
|
5797
|
-
features: z6.array(z6.string()).optional().catch(void 0),
|
|
5798
|
-
addresses: z6.array(z6.string()).optional().catch(void 0)
|
|
5799
|
-
}).optional().catch(void 0)
|
|
5800
|
-
}),
|
|
5801
|
-
// `claudeVoice`: whether this machine can play a Claude voice dictation (see voiceStart).
|
|
5802
|
-
// An older agent omits it, and the PWA then treats Claude voice as unavailable there.
|
|
5803
|
-
z6.object({ type: z6.literal("workspaces"), workspaces: z6.array(workspaceSchema), claudeVoice: z6.boolean().optional() }),
|
|
5804
|
-
z6.object({ type: z6.literal("workspaceUpdated"), workspace: workspaceSchema }),
|
|
5805
|
-
z6.object({ type: z6.literal("recentPaths"), paths: z6.array(z6.string()) }),
|
|
5806
|
-
// The agent's configured project roots — the folder picker's top-level browse points.
|
|
5807
|
-
// `roots` is additive: older agents omit it, so it defaults to [] rather than letting the
|
|
5808
|
-
// strict client parser drop the whole message during a version-skewed rollout.
|
|
5809
|
-
z6.object({ type: z6.literal("roots"), roots: z6.array(z6.string()).default([]) }),
|
|
5810
|
-
// What the Create Workspace command list offers on this machine: the known CLIs found
|
|
5811
|
-
// installed (`claude`, `codex`) and the custom commands launched here before. Additive like
|
|
5812
|
-
// `roots`: an older agent never sends it and the PWA keeps its fixed list.
|
|
5813
|
-
z6.object({ type: z6.literal("launchers"), detected: z6.array(z6.string()).default([]), custom: z6.array(z6.string()).default([]) }),
|
|
5814
|
-
// The agent machine's dictation vocabulary (~/.tarfa/keyterms.txt), passed to ElevenLabs as
|
|
5815
|
-
// keyterms so jargon transcribes correctly. Additive like `roots`: older agents omit it,
|
|
5816
|
-
// so it defaults to [] instead of letting the strict client parser drop the whole message.
|
|
5817
|
-
z6.object({ type: z6.literal("keyterms"), keyterms: z6.array(z6.string()).default([]) }),
|
|
5818
|
-
// The dictation provider keys the agent's operator stored (~/.tarfa/dictation-keys.json), sent
|
|
5819
|
-
// only in reply to getDictationKeys. A key the agent does not hold is absent.
|
|
5820
|
-
z6.object({
|
|
5821
|
-
type: z6.literal("dictationKeys"),
|
|
5822
|
-
elevenlabs: z6.string().optional().catch(void 0),
|
|
5823
|
-
openrouter: z6.string().optional().catch(void 0)
|
|
5824
|
-
}),
|
|
5825
|
-
// The answer to setDictationKey. `error` is the agent's fixed wording of a refusal and never
|
|
5826
|
-
// holds the key.
|
|
5827
|
-
z6.object({ type: z6.literal("dictationKeySet"), id: z6.string(), provider: dictationProviderSchema, ok: z6.boolean(), error: z6.string().optional() }),
|
|
5828
|
-
// `files` is additive: older agents that predate it omit the field, so it defaults to []
|
|
5829
|
-
// (a strictly-required field would make the strict client parser silently drop their
|
|
5830
|
-
// dirEntries during a version-skewed rollout, hanging the browser on "Loading…").
|
|
5831
|
-
z6.object({ type: z6.literal("dirEntries"), path: z6.string(), subdirs: z6.array(z6.string()), files: z6.array(z6.string()).default([]) }),
|
|
5832
|
-
// `truncated`/`binary` are additive: older agents omit them, so they default to false (a
|
|
5833
|
-
// strictly-required field would make the strict client parser drop the whole message
|
|
5834
|
-
// during a version-skewed rollout).
|
|
5835
|
-
z6.object({
|
|
5836
|
-
type: z6.literal("fileContents"),
|
|
5837
|
-
path: z6.string(),
|
|
5838
|
-
data: z6.string(),
|
|
5839
|
-
truncated: z6.boolean().default(false),
|
|
5840
|
-
binary: z6.boolean().default(false)
|
|
5841
|
-
}),
|
|
5842
|
-
z6.object({ type: z6.literal("attached"), workspaceId: z6.string() }),
|
|
5843
|
-
z6.object({ type: z6.literal("output"), workspaceId: z6.string(), data: z6.string() }),
|
|
5844
|
-
z6.object({ type: z6.literal("fileStored"), workspaceId: z6.string(), path: z6.string() }),
|
|
5845
|
-
// `text` is null when the agent holds no reply for that workspace (none since it started, or not Claude).
|
|
5846
|
-
z6.object({ type: z6.literal("lastReply"), workspaceId: z6.string(), text: z6.string().nullable() }),
|
|
5847
|
-
z6.object({ type: z6.literal("voiceDone"), id: z6.string(), ok: z6.boolean(), error: z6.string().optional() }),
|
|
5848
|
-
z6.object({ type: z6.literal("detached"), workspaceId: z6.string(), reason: detachReasonSchema }),
|
|
5849
|
-
z6.object({ type: z6.literal("closed"), workspaceId: z6.string() }),
|
|
5850
|
-
// A phone was just enrolled on this machine (a pairing code), told to every other
|
|
5851
|
-
// phone connected to it: a device nobody meant to add must not go unnoticed. An older PWA drops
|
|
5852
|
-
// the unknown type.
|
|
5853
|
-
z6.object({ type: z6.literal("deviceEnrolled"), id: z6.string(), name: z6.string(), at: z6.string() }),
|
|
5854
|
-
z6.object({ type: z6.literal("error"), code: z6.string(), message: z6.string() }),
|
|
5855
|
-
// Live agent-browser sessions, sent after every `workspaces` reply. `sessions` defaults
|
|
5856
|
-
// to [] so a skewed sender omitting it can't make the strict client parser drop the
|
|
5857
|
-
// whole message.
|
|
5858
|
-
z6.object({ type: z6.literal("browserSessions"), sessions: z6.array(browserSessionSchema).default([]) }),
|
|
5859
|
-
// The agent machine's current CPU/memory load (see machineStatsSchema). Pushed on a
|
|
5860
|
-
// timer while at least one client is connected; an agent that predates it simply never
|
|
5861
|
-
// sends one and the PWA shows no load strip for that machine.
|
|
5862
|
-
z6.object({ type: z6.literal("stats"), stats: machineStatsSchema }),
|
|
5863
|
-
// Reply to the client's liveness `ping` (see clientMessageSchema).
|
|
5864
|
-
z6.object({ type: z6.literal("pong") }),
|
|
5865
|
-
...sessionAgentMessageSchemas
|
|
5866
|
-
]);
|
|
5867
|
-
function parseClientMessage(raw) {
|
|
5868
|
-
return clientMessageSchema.parse(raw);
|
|
5920
|
+
// src/application/agent-env-location.ts
|
|
5921
|
+
import { join as join6 } from "node:path";
|
|
5922
|
+
function resolveAgentEnvPath(input) {
|
|
5923
|
+
if (input.env.TARFA_AGENT_ENV) return input.env.TARFA_AGENT_ENV;
|
|
5924
|
+
const stateFile = join6(input.home, ".tarfa", "agent.env");
|
|
5925
|
+
if (input.exists(stateFile) || input.checkout === null) return stateFile;
|
|
5926
|
+
return join6(input.checkout, "deploy", "agent.env");
|
|
5869
5927
|
}
|
|
5870
5928
|
|
|
5871
|
-
//
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
z7.object({ type: z7.literal("auth"), token: z7.string() }),
|
|
5876
|
-
// Liveness probe, same pattern as the main terminal socket.
|
|
5877
|
-
z7.object({ type: z7.literal("ping") }),
|
|
5878
|
-
z7.object({
|
|
5879
|
-
type: z7.literal("input_mouse"),
|
|
5880
|
-
eventType: z7.string(),
|
|
5881
|
-
x: z7.number(),
|
|
5882
|
-
y: z7.number(),
|
|
5883
|
-
button: z7.string().optional(),
|
|
5884
|
-
clickCount: z7.number().int().optional(),
|
|
5885
|
-
deltaX: z7.number().optional(),
|
|
5886
|
-
deltaY: z7.number().optional(),
|
|
5887
|
-
modifiers
|
|
5888
|
-
}),
|
|
5889
|
-
z7.object({
|
|
5890
|
-
type: z7.literal("input_keyboard"),
|
|
5891
|
-
eventType: z7.string(),
|
|
5892
|
-
key: z7.string().optional(),
|
|
5893
|
-
code: z7.string().optional(),
|
|
5894
|
-
text: z7.string().optional(),
|
|
5895
|
-
windowsVirtualKeyCode: z7.number().int().optional(),
|
|
5896
|
-
modifiers
|
|
5897
|
-
}),
|
|
5898
|
-
z7.object({
|
|
5899
|
-
type: z7.literal("input_touch"),
|
|
5900
|
-
eventType: z7.string(),
|
|
5901
|
-
touchPoints: z7.array(z7.object({ x: z7.number(), y: z7.number() }).passthrough()),
|
|
5902
|
-
modifiers
|
|
5903
|
-
}),
|
|
5904
|
-
// Handled by the agent (CLI navigation/session control), never forwarded upstream.
|
|
5905
|
-
z7.object({ type: z7.literal("navigate"), url: z7.string() }),
|
|
5906
|
-
z7.object({ type: z7.literal("back") }),
|
|
5907
|
-
z7.object({ type: z7.literal("forward") }),
|
|
5908
|
-
z7.object({ type: z7.literal("closeSession") }),
|
|
5909
|
-
// Resize the remote browser viewport to the attached viewer's canvas (CSS pixels).
|
|
5910
|
-
z7.object({
|
|
5911
|
-
type: z7.literal("setViewport"),
|
|
5912
|
-
width: z7.number().int().positive(),
|
|
5913
|
-
height: z7.number().int().positive()
|
|
5914
|
-
})
|
|
5915
|
-
]);
|
|
5916
|
-
var browserEndedReasonSchema = z7.enum(["closed", "not-found", "error", "opened-elsewhere"]);
|
|
5917
|
-
var browserAgentMessageSchema = z7.discriminatedUnion("type", [
|
|
5918
|
-
z7.object({ type: z7.literal("authResult"), ok: z7.boolean() }),
|
|
5919
|
-
z7.object({ type: z7.literal("pong") }),
|
|
5920
|
-
z7.object({ type: z7.literal("ended"), reason: browserEndedReasonSchema }),
|
|
5921
|
-
z7.object({
|
|
5922
|
-
type: z7.literal("frame"),
|
|
5923
|
-
data: z7.string(),
|
|
5924
|
-
// base64 JPEG
|
|
5925
|
-
metadata: z7.object({ deviceWidth: z7.number(), deviceHeight: z7.number() }).passthrough()
|
|
5926
|
-
}).passthrough(),
|
|
5927
|
-
z7.object({ type: z7.literal("tabs") }).passthrough(),
|
|
5928
|
-
z7.object({ type: z7.literal("url"), url: z7.string() }).passthrough(),
|
|
5929
|
-
z7.object({ type: z7.literal("status") }).passthrough()
|
|
5930
|
-
]);
|
|
5931
|
-
function parseBrowserClientMessage(raw) {
|
|
5932
|
-
return browserClientMessageSchema.parse(raw);
|
|
5929
|
+
// src/infrastructure/agent-env-file.ts
|
|
5930
|
+
var exec3 = promisify3(execFile3);
|
|
5931
|
+
function withoutGitVars(env) {
|
|
5932
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith("GIT_")));
|
|
5933
5933
|
}
|
|
5934
|
-
function
|
|
5935
|
-
|
|
5934
|
+
async function mainCheckout(kind2 = installKind()) {
|
|
5935
|
+
if (kind2 === "package") return null;
|
|
5936
|
+
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
5937
|
+
try {
|
|
5938
|
+
const { stdout } = await exec3("git", ["-C", here, "rev-parse", "--path-format=absolute", "--git-common-dir"], { env: withoutGitVars(process.env) });
|
|
5939
|
+
return dirname8(stdout.trim());
|
|
5940
|
+
} catch {
|
|
5941
|
+
return resolve5(here, "..", "..", "..", "..");
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
async function locateAgentEnv(env, home, kind2 = installKind()) {
|
|
5945
|
+
return resolveAgentEnvPath({ env, home, checkout: await mainCheckout(kind2), exists: existsSync3 });
|
|
5946
|
+
}
|
|
5947
|
+
function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
|
|
5948
|
+
return {
|
|
5949
|
+
path,
|
|
5950
|
+
read: async () => {
|
|
5951
|
+
try {
|
|
5952
|
+
return await readFile8(path, "utf8");
|
|
5953
|
+
} catch (err) {
|
|
5954
|
+
if (err.code === "ENOENT") {
|
|
5955
|
+
throw new Error(`no agent config at ${path}: set the agent up first (docs/DEPLOY.md), or set TARFA_AGENT_ENV`);
|
|
5956
|
+
}
|
|
5957
|
+
throw err;
|
|
5958
|
+
}
|
|
5959
|
+
},
|
|
5960
|
+
write: async (text) => {
|
|
5961
|
+
let backup = `${path}.bak-${now().toISOString().replace(/[:.]/g, "-")}`;
|
|
5962
|
+
try {
|
|
5963
|
+
await copyFile(path, backup);
|
|
5964
|
+
await chmod2(backup, 384);
|
|
5965
|
+
} catch (err) {
|
|
5966
|
+
if (err.code !== "ENOENT") throw err;
|
|
5967
|
+
backup = null;
|
|
5968
|
+
}
|
|
5969
|
+
await mkdir7(dirname8(path), { recursive: true, mode: 448 });
|
|
5970
|
+
await writeFile5(path, text, { mode: 384 });
|
|
5971
|
+
await chmod2(path, 384);
|
|
5972
|
+
return backup;
|
|
5973
|
+
}
|
|
5974
|
+
};
|
|
5936
5975
|
}
|
|
5937
5976
|
|
|
5938
5977
|
// src/infrastructure/file-status-store.ts
|
|
5978
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
|
|
5979
|
+
import { dirname as dirname9 } from "node:path";
|
|
5939
5980
|
var FileStatusStore = class {
|
|
5940
5981
|
constructor(filePath, now = Date.now) {
|
|
5941
5982
|
this.filePath = filePath;
|
|
@@ -6510,6 +6551,7 @@ export {
|
|
|
6510
6551
|
browseDir,
|
|
6511
6552
|
atomicWrite,
|
|
6512
6553
|
loadOrCreateAgentKeys,
|
|
6554
|
+
readAgentPublicKey,
|
|
6513
6555
|
enrolmentMode,
|
|
6514
6556
|
withEnrolmentMode,
|
|
6515
6557
|
standingOf,
|
|
@@ -6517,6 +6559,17 @@ export {
|
|
|
6517
6559
|
revokeDevice,
|
|
6518
6560
|
forgetDevice,
|
|
6519
6561
|
FileDeviceStore,
|
|
6562
|
+
isWorkspaceStatus,
|
|
6563
|
+
runsClaude,
|
|
6564
|
+
imageMediaType,
|
|
6565
|
+
pdfMediaType,
|
|
6566
|
+
audioMediaType,
|
|
6567
|
+
PROTOCOL_VERSION,
|
|
6568
|
+
AGENT_FEATURES,
|
|
6569
|
+
parseClientMessage,
|
|
6570
|
+
parseBrowserClientMessage,
|
|
6571
|
+
parseBrowserAgentMessage,
|
|
6572
|
+
workspaceLink,
|
|
6520
6573
|
DICTATION_PROVIDERS,
|
|
6521
6574
|
providerOf,
|
|
6522
6575
|
changeDictationKey,
|
|
@@ -6531,16 +6584,6 @@ export {
|
|
|
6531
6584
|
mainCheckout,
|
|
6532
6585
|
locateAgentEnv,
|
|
6533
6586
|
agentEnvFile,
|
|
6534
|
-
isWorkspaceStatus,
|
|
6535
|
-
runsClaude,
|
|
6536
|
-
imageMediaType,
|
|
6537
|
-
pdfMediaType,
|
|
6538
|
-
audioMediaType,
|
|
6539
|
-
PROTOCOL_VERSION,
|
|
6540
|
-
AGENT_FEATURES,
|
|
6541
|
-
parseClientMessage,
|
|
6542
|
-
parseBrowserClientMessage,
|
|
6543
|
-
parseBrowserAgentMessage,
|
|
6544
6587
|
FileStatusStore,
|
|
6545
6588
|
FileParkedStore,
|
|
6546
6589
|
FsFileWriter,
|
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
planCli,
|
|
60
60
|
planSetup,
|
|
61
61
|
providerOf,
|
|
62
|
+
readAgentPublicKey,
|
|
62
63
|
renderService,
|
|
63
64
|
resolveCodexHome,
|
|
64
65
|
resolveStateDir,
|
|
@@ -70,8 +71,9 @@ import {
|
|
|
70
71
|
systemPathResolver,
|
|
71
72
|
unitRunsLauncher,
|
|
72
73
|
withEnrolmentMode,
|
|
73
|
-
withLegacyEnv
|
|
74
|
-
|
|
74
|
+
withLegacyEnv,
|
|
75
|
+
workspaceLink
|
|
76
|
+
} from "./chunk-4M2ZRGQR.js";
|
|
75
77
|
|
|
76
78
|
// src/cli.ts
|
|
77
79
|
import { randomBytes } from "node:crypto";
|
|
@@ -1196,7 +1198,7 @@ async function run3(argv, rawEnv, home, io = {}) {
|
|
|
1196
1198
|
throw new UsageError(RELAYS_GONE);
|
|
1197
1199
|
}
|
|
1198
1200
|
if (argv[0] === "pair") return pair(argv.slice(1), env, home);
|
|
1199
|
-
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") {
|
|
1200
1202
|
return orchestration(argv[0], argv.slice(1), env, home, io);
|
|
1201
1203
|
}
|
|
1202
1204
|
const config = loadConfig({ ...env, ...await agentEnvValues(env, home) }, home);
|
|
@@ -1288,6 +1290,7 @@ Workspaces on this machine:
|
|
|
1288
1290
|
send send a message to another workspace
|
|
1289
1291
|
unpark reopen parked workspaces
|
|
1290
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)
|
|
1291
1294
|
close close workspaces
|
|
1292
1295
|
list, create, recent, browse the agent's own views, as JSON
|
|
1293
1296
|
|
|
@@ -1307,6 +1310,7 @@ var COMMANDS = /* @__PURE__ */ new Set([
|
|
|
1307
1310
|
"peek",
|
|
1308
1311
|
"unpark",
|
|
1309
1312
|
"rename",
|
|
1313
|
+
"link",
|
|
1310
1314
|
"send",
|
|
1311
1315
|
"close",
|
|
1312
1316
|
"wait",
|
|
@@ -1321,6 +1325,7 @@ var SPAWN_CLIS = ["claude", "codex", "pi"];
|
|
|
1321
1325
|
var PEEK_USAGE = "usage: tarfa peek <workspace id> [-n <lines>]";
|
|
1322
1326
|
var UNPARK_USAGE = "usage: tarfa unpark <workspace id>...";
|
|
1323
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)";
|
|
1324
1329
|
var SEND_USAGE = 'usage: tarfa send <workspace id> "<message>" (or the message on stdin)';
|
|
1325
1330
|
var CLOSE_USAGE = "usage: tarfa close <workspace id>...";
|
|
1326
1331
|
var WAIT_USAGE = "usage: tarfa wait <workspace id>... [--timeout <seconds, at least 10>]";
|
|
@@ -1483,6 +1488,18 @@ async function orchestration(command, argv, env, home, io) {
|
|
|
1483
1488
|
const done = `${id}: renamed to "${title.trim()}"`;
|
|
1484
1489
|
return locked ? done : `${done} (this tmux cannot lock a title, so Claude may rename it again)`;
|
|
1485
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
|
+
}
|
|
1486
1503
|
case "close": {
|
|
1487
1504
|
if (args.length === 0 || args.some((id) => id.startsWith("-"))) throw new UsageError(CLOSE_USAGE);
|
|
1488
1505
|
const self = await callerWorkspace(env, tmux, config.sessionPrefix).catch(() => void 0);
|
package/dist/serve.js
CHANGED