@tarfa/cli 0.1.6 → 0.1.8

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.
@@ -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,440 @@ 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
+ // Throw the dictation away: Claude writes nothing. Sent only to an agent whose authResult names
5530
+ // the `voiceCancel` feature (an older one would drop it, and Claude would type what it heard).
5531
+ z5.object({ type: z5.literal("voiceCancel"), id: z5.string() }),
5532
+ // The text of Claude's last reply in a workspace, read aloud on the phone. Sent only to an agent
5533
+ // whose authResult names the `lastReply` feature.
5534
+ z5.object({ type: z5.literal("lastReply"), workspaceId: z5.string() }),
5535
+ z5.object({ type: z5.literal("resize"), workspaceId: z5.string(), cols: dim, rows: dim }),
5536
+ // `remember` is set only when the command came from the Custom field: the agent then adds it
5537
+ // to the machine's launcher list. Additive, so an older agent just ignores it.
5538
+ z5.object({ type: z5.literal("create"), projectPath: z5.string(), command: z5.string(), remember: z5.boolean().optional() }),
5539
+ z5.object({ type: z5.literal("close"), workspaceId: z5.string() }),
5540
+ z5.object({ type: z5.literal("setStatus"), workspaceId: z5.string(), status: workspaceStatusSchema }),
5541
+ // Manually pin/unpin a workspace to the top of its status group. Orthogonal to setStatus;
5542
+ // the agent's onChange broadcasts the refreshed workspace to every device.
5543
+ z5.object({ type: z5.literal("setUrgent"), workspaceId: z5.string(), urgent: z5.boolean() }),
5544
+ // Start an agent-browser session named after the workspace. Old agents answer with their
5545
+ // generic unknown-message error; the PWA only offers this once the agent has proven
5546
+ // support by sending `browserSessions`.
5547
+ z5.object({ type: z5.literal("startBrowser"), workspaceId: z5.string() }),
5548
+ // Liveness probe: the PWA pings periodically so it can detect a half-open socket that
5549
+ // never fires a `close` event (common on mobile) and force a reconnect. The agent answers
5550
+ // with `pong`.
5551
+ z5.object({ type: z5.literal("ping") }),
5552
+ ...sessionClientMessageSchemas
5553
+ ]);
5554
+ var detachReasonSchema = z5.enum(["opened-elsewhere", "closed", "error"]);
5555
+ var PROTOCOL_VERSION = 1;
5556
+ var AGENT_FEATURES = {
5557
+ lastReply: "lastReply",
5558
+ dictationKeys: "dictationKeys",
5559
+ setDictationKey: "setDictationKey",
5560
+ addresses: "addresses",
5561
+ voiceCancel: "voiceCancel"
5562
+ };
5563
+ var agentMessageSchema = z5.discriminatedUnion("type", [
5564
+ // `credential` is what authenticated the session (`token` only from an agent from before token
5565
+ // enrolment was removed); `enrolled` says the device key is enrolled (written), so the phone
5566
+ // forgets its pairing secret. Both optional: an older agent sends neither.
5567
+ // `pushed`: the agent served the phone at once (its hello asked) and has sent the opening replies
5568
+ // (workspaces, keyterms) behind it, so the phone does not ask for them.
5569
+ z5.object({
5570
+ type: z5.literal("authResult"),
5571
+ ok: z5.boolean(),
5572
+ credential: z5.enum(["device", "pairing", "token"]).optional().catch(void 0),
5573
+ enrolled: z5.boolean().optional().catch(void 0),
5574
+ pushed: z5.literal(true).optional().catch(void 0),
5575
+ // `features` names what this agent answers beyond the protocol, so the PWA offers a control
5576
+ // only to an agent that supports it (a worktree agent shares a release's version number).
5577
+ // `addresses`: where phones reach this agent now (its TARFA_PHONE_ADDRESSES), which a phone
5578
+ // adopts only once it has connected through each one.
5579
+ agent: z5.object({
5580
+ protocol: z5.number().int(),
5581
+ version: z5.string().optional(),
5582
+ features: z5.array(z5.string()).optional().catch(void 0),
5583
+ addresses: z5.array(z5.string()).optional().catch(void 0)
5584
+ }).optional().catch(void 0)
5585
+ }),
5586
+ // `claudeVoice`: whether this machine can play a Claude voice dictation (see voiceStart).
5587
+ // An older agent omits it, and the PWA then treats Claude voice as unavailable there.
5588
+ z5.object({ type: z5.literal("workspaces"), workspaces: z5.array(workspaceSchema), claudeVoice: z5.boolean().optional() }),
5589
+ z5.object({ type: z5.literal("workspaceUpdated"), workspace: workspaceSchema }),
5590
+ z5.object({ type: z5.literal("recentPaths"), paths: z5.array(z5.string()) }),
5591
+ // The agent's configured project roots — the folder picker's top-level browse points.
5592
+ // `roots` is additive: older agents omit it, so it defaults to [] rather than letting the
5593
+ // strict client parser drop the whole message during a version-skewed rollout.
5594
+ z5.object({ type: z5.literal("roots"), roots: z5.array(z5.string()).default([]) }),
5595
+ // What the Create Workspace command list offers on this machine: the known CLIs found
5596
+ // installed (`claude`, `codex`) and the custom commands launched here before. Additive like
5597
+ // `roots`: an older agent never sends it and the PWA keeps its fixed list.
5598
+ z5.object({ type: z5.literal("launchers"), detected: z5.array(z5.string()).default([]), custom: z5.array(z5.string()).default([]) }),
5599
+ // The agent machine's dictation vocabulary (~/.tarfa/keyterms.txt), passed to ElevenLabs as
5600
+ // keyterms so jargon transcribes correctly. Additive like `roots`: older agents omit it,
5601
+ // so it defaults to [] instead of letting the strict client parser drop the whole message.
5602
+ z5.object({ type: z5.literal("keyterms"), keyterms: z5.array(z5.string()).default([]) }),
5603
+ // The dictation provider keys the agent's operator stored (~/.tarfa/dictation-keys.json), sent
5604
+ // only in reply to getDictationKeys. A key the agent does not hold is absent.
5605
+ z5.object({
5606
+ type: z5.literal("dictationKeys"),
5607
+ elevenlabs: z5.string().optional().catch(void 0),
5608
+ openrouter: z5.string().optional().catch(void 0)
5609
+ }),
5610
+ // The answer to setDictationKey. `error` is the agent's fixed wording of a refusal and never
5611
+ // holds the key.
5612
+ z5.object({ type: z5.literal("dictationKeySet"), id: z5.string(), provider: dictationProviderSchema, ok: z5.boolean(), error: z5.string().optional() }),
5613
+ // `files` is additive: older agents that predate it omit the field, so it defaults to []
5614
+ // (a strictly-required field would make the strict client parser silently drop their
5615
+ // dirEntries during a version-skewed rollout, hanging the browser on "Loading…").
5616
+ z5.object({ type: z5.literal("dirEntries"), path: z5.string(), subdirs: z5.array(z5.string()), files: z5.array(z5.string()).default([]) }),
5617
+ // `truncated`/`binary` are additive: older agents omit them, so they default to false (a
5618
+ // strictly-required field would make the strict client parser drop the whole message
5619
+ // during a version-skewed rollout).
5620
+ z5.object({
5621
+ type: z5.literal("fileContents"),
5622
+ path: z5.string(),
5623
+ data: z5.string(),
5624
+ truncated: z5.boolean().default(false),
5625
+ binary: z5.boolean().default(false)
5626
+ }),
5627
+ z5.object({ type: z5.literal("attached"), workspaceId: z5.string() }),
5628
+ z5.object({ type: z5.literal("output"), workspaceId: z5.string(), data: z5.string() }),
5629
+ z5.object({ type: z5.literal("fileStored"), workspaceId: z5.string(), path: z5.string() }),
5630
+ // `text` is null when the agent holds no reply for that workspace (none since it started, or not Claude).
5631
+ z5.object({ type: z5.literal("lastReply"), workspaceId: z5.string(), text: z5.string().nullable() }),
5632
+ z5.object({ type: z5.literal("voiceDone"), id: z5.string(), ok: z5.boolean(), error: z5.string().optional() }),
5633
+ z5.object({ type: z5.literal("detached"), workspaceId: z5.string(), reason: detachReasonSchema }),
5634
+ z5.object({ type: z5.literal("closed"), workspaceId: z5.string() }),
5635
+ // A phone was just enrolled on this machine (a pairing code), told to every other
5636
+ // phone connected to it: a device nobody meant to add must not go unnoticed. An older PWA drops
5637
+ // the unknown type.
5638
+ z5.object({ type: z5.literal("deviceEnrolled"), id: z5.string(), name: z5.string(), at: z5.string() }),
5639
+ z5.object({ type: z5.literal("error"), code: z5.string(), message: z5.string() }),
5640
+ // Live agent-browser sessions, sent after every `workspaces` reply. `sessions` defaults
5641
+ // to [] so a skewed sender omitting it can't make the strict client parser drop the
5642
+ // whole message.
5643
+ z5.object({ type: z5.literal("browserSessions"), sessions: z5.array(browserSessionSchema).default([]) }),
5644
+ // The agent machine's current CPU/memory load (see machineStatsSchema). Pushed on a
5645
+ // timer while at least one client is connected; an agent that predates it simply never
5646
+ // sends one and the PWA shows no load strip for that machine.
5647
+ z5.object({ type: z5.literal("stats"), stats: machineStatsSchema }),
5648
+ // Reply to the client's liveness `ping` (see clientMessageSchema).
5649
+ z5.object({ type: z5.literal("pong") }),
5650
+ ...sessionAgentMessageSchemas
5651
+ ]);
5652
+ function parseClientMessage(raw) {
5653
+ return clientMessageSchema.parse(raw);
5654
+ }
5655
+
5656
+ // ../contracts/src/protocol/browser-messages.ts
5657
+ import { z as z6 } from "zod";
5658
+ var modifiers = z6.number().int().optional();
5659
+ var browserClientMessageSchema = z6.discriminatedUnion("type", [
5660
+ z6.object({ type: z6.literal("auth"), token: z6.string() }),
5661
+ // Liveness probe, same pattern as the main terminal socket.
5662
+ z6.object({ type: z6.literal("ping") }),
5663
+ z6.object({
5664
+ type: z6.literal("input_mouse"),
5665
+ eventType: z6.string(),
5666
+ x: z6.number(),
5667
+ y: z6.number(),
5668
+ button: z6.string().optional(),
5669
+ clickCount: z6.number().int().optional(),
5670
+ deltaX: z6.number().optional(),
5671
+ deltaY: z6.number().optional(),
5672
+ modifiers
5673
+ }),
5674
+ z6.object({
5675
+ type: z6.literal("input_keyboard"),
5676
+ eventType: z6.string(),
5677
+ key: z6.string().optional(),
5678
+ code: z6.string().optional(),
5679
+ text: z6.string().optional(),
5680
+ windowsVirtualKeyCode: z6.number().int().optional(),
5681
+ modifiers
5682
+ }),
5683
+ z6.object({
5684
+ type: z6.literal("input_touch"),
5685
+ eventType: z6.string(),
5686
+ touchPoints: z6.array(z6.object({ x: z6.number(), y: z6.number() }).passthrough()),
5687
+ modifiers
5688
+ }),
5689
+ // Handled by the agent (CLI navigation/session control), never forwarded upstream.
5690
+ z6.object({ type: z6.literal("navigate"), url: z6.string() }),
5691
+ z6.object({ type: z6.literal("back") }),
5692
+ z6.object({ type: z6.literal("forward") }),
5693
+ z6.object({ type: z6.literal("closeSession") }),
5694
+ // Resize the remote browser viewport to the attached viewer's canvas (CSS pixels).
5695
+ z6.object({
5696
+ type: z6.literal("setViewport"),
5697
+ width: z6.number().int().positive(),
5698
+ height: z6.number().int().positive()
5699
+ })
5700
+ ]);
5701
+ var browserEndedReasonSchema = z6.enum(["closed", "not-found", "error", "opened-elsewhere"]);
5702
+ var browserAgentMessageSchema = z6.discriminatedUnion("type", [
5703
+ z6.object({ type: z6.literal("authResult"), ok: z6.boolean() }),
5704
+ z6.object({ type: z6.literal("pong") }),
5705
+ z6.object({ type: z6.literal("ended"), reason: browserEndedReasonSchema }),
5706
+ z6.object({
5707
+ type: z6.literal("frame"),
5708
+ data: z6.string(),
5709
+ // base64 JPEG
5710
+ metadata: z6.object({ deviceWidth: z6.number(), deviceHeight: z6.number() }).passthrough()
5711
+ }).passthrough(),
5712
+ z6.object({ type: z6.literal("tabs") }).passthrough(),
5713
+ z6.object({ type: z6.literal("url"), url: z6.string() }).passthrough(),
5714
+ z6.object({ type: z6.literal("status") }).passthrough()
5715
+ ]);
5716
+ function parseBrowserClientMessage(raw) {
5717
+ return browserClientMessageSchema.parse(raw);
5718
+ }
5719
+ function parseBrowserAgentMessage(raw) {
5720
+ return browserAgentMessageSchema.parse(raw);
5721
+ }
5722
+
5279
5723
  // src/application/dictation-keys.ts
5280
5724
  var DICTATION_PROVIDERS = ["elevenlabs", "openrouter"];
5281
5725
  var MAX_KEY_LENGTH = 512;
@@ -5289,6 +5733,16 @@ function checkKey(raw) {
5289
5733
  if (key2.length > MAX_KEY_LENGTH) return { ok: false, error: `a key is at most ${MAX_KEY_LENGTH} characters` };
5290
5734
  return { ok: true, key: key2 };
5291
5735
  }
5736
+ var SHAPE = {
5737
+ elevenlabs: "an ElevenLabs key starts with sk_ followed by at least 32 letters and digits",
5738
+ openrouter: "an OpenRouter key starts with sk-or-v1- followed by at least 32 letters and digits"
5739
+ };
5740
+ var OTHER = { elevenlabs: "OpenRouter", openrouter: "ElevenLabs" };
5741
+ function keyFormatError(provider, key2) {
5742
+ const problem = dictationKeyProblem(provider, key2);
5743
+ if (problem === null) return null;
5744
+ return problem === "other-provider" ? `this looks like an ${OTHER[provider]} key; ${SHAPE[provider]}` : SHAPE[provider];
5745
+ }
5292
5746
  function parseDictationKeys(raw) {
5293
5747
  let value;
5294
5748
  try {
@@ -5318,6 +5772,8 @@ async function changeDictationKey(store, provider, raw) {
5318
5772
  if (raw !== null) {
5319
5773
  const checked = checkKey(raw);
5320
5774
  if (!checked.ok) return checked;
5775
+ const formatError = keyFormatError(provider, checked.key);
5776
+ if (formatError) return { ok: false, error: formatError };
5321
5777
  key2 = checked.key;
5322
5778
  }
5323
5779
  const result = await store.update((keys) => key2 === null ? withoutKey(keys, provider) : withKey(keys, provider, key2));
@@ -5409,8 +5865,8 @@ function pairingName(machineId, hostname) {
5409
5865
  return GENERIC_MACHINE_IDS.has(machineId) ? hostname.split(".")[0] : machineId;
5410
5866
  }
5411
5867
  function announcedAddresses(phoneAddresses2) {
5412
- const valid = phoneAddresses2.length > 0 && phoneAddresses2.length <= MAX_ADDRESSES && phoneAddresses2.every(isMachineUrl);
5413
- return valid ? phoneAddresses2 : void 0;
5868
+ const valid2 = phoneAddresses2.length > 0 && phoneAddresses2.length <= MAX_ADDRESSES && phoneAddresses2.every(isMachineUrl);
5869
+ return valid2 ? phoneAddresses2 : void 0;
5414
5870
  }
5415
5871
  async function mintPairing(deps, input) {
5416
5872
  if (deps.defaultPath !== void 0 && !isPairingPath(deps.defaultPath)) {
@@ -5429,8 +5885,8 @@ async function mintPairing(deps, input) {
5429
5885
 
5430
5886
  // src/infrastructure/file-pairing-store.ts
5431
5887
  import { readFile as readFile7 } from "node:fs/promises";
5432
- import { z as z3 } from "zod";
5433
- var schema = z3.array(z3.object({ secret: z3.string().regex(/^[A-Za-z0-9_-]{43}$/), expiresAt: z3.string(), usedAt: z3.string().optional(), usedBy: z3.string().optional() }));
5888
+ import { z as z7 } from "zod";
5889
+ 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
5890
  var FilePairingStore = class {
5435
5891
  constructor(filePath, staleLockMs = 1e4) {
5436
5892
  this.filePath = filePath;
@@ -5452,490 +5908,79 @@ var FilePairingStore = class {
5452
5908
  const next = change(current);
5453
5909
  if (JSON.stringify(next) !== JSON.stringify(current)) await writeFileAtomic(this.filePath, `${JSON.stringify(next, null, 2)}
5454
5910
  `);
5455
- 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"
5911
+ return next;
5912
+ });
5913
+ }
5588
5914
  };
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
5915
 
5627
- // ../contracts/src/protocol/session-channels.ts
5628
- var id = z5.string().min(1).max(64);
5629
- var mediaResultSchema = z5.enum(["ok", "forbidden", "not-found", "too-large", "error"]);
5630
- var sessionClientMessageSchemas = [
5631
- z5.object({ type: z5.literal("readMedia"), id, path: z5.string() }),
5632
- z5.object({ type: z5.literal("mediaAck"), id, seq: z5.number().int().nonnegative() }),
5633
- z5.object({ type: z5.literal("mediaCancel"), id }),
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
- ];
5916
+ // src/infrastructure/agent-env-file.ts
5917
+ import { execFile as execFile3 } from "node:child_process";
5918
+ import { existsSync as existsSync3 } from "node:fs";
5919
+ import { chmod as chmod2, copyFile, mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "node:fs/promises";
5920
+ import { dirname as dirname8, resolve as resolve5 } from "node:path";
5921
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
5922
+ import { promisify as promisify3 } from "node:util";
5661
5923
 
5662
- // ../contracts/src/protocol/messages.ts
5663
- var workspaceStatusSchema = z6.enum(
5664
- WORKSPACE_STATUSES
5665
- );
5666
- var workspaceSchema = z6.object({
5667
- machineId: z6.string(),
5668
- id: z6.string(),
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);
5924
+ // src/application/agent-env-location.ts
5925
+ import { join as join6 } from "node:path";
5926
+ function resolveAgentEnvPath(input) {
5927
+ if (input.env.TARFA_AGENT_ENV) return input.env.TARFA_AGENT_ENV;
5928
+ const stateFile = join6(input.home, ".tarfa", "agent.env");
5929
+ if (input.exists(stateFile) || input.checkout === null) return stateFile;
5930
+ return join6(input.checkout, "deploy", "agent.env");
5869
5931
  }
5870
5932
 
5871
- // ../contracts/src/protocol/browser-messages.ts
5872
- import { z as z7 } from "zod";
5873
- var modifiers = z7.number().int().optional();
5874
- var browserClientMessageSchema = z7.discriminatedUnion("type", [
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);
5933
+ // src/infrastructure/agent-env-file.ts
5934
+ var exec3 = promisify3(execFile3);
5935
+ function withoutGitVars(env) {
5936
+ return Object.fromEntries(Object.entries(env).filter(([name]) => !name.startsWith("GIT_")));
5933
5937
  }
5934
- function parseBrowserAgentMessage(raw) {
5935
- return browserAgentMessageSchema.parse(raw);
5938
+ async function mainCheckout(kind2 = installKind()) {
5939
+ if (kind2 === "package") return null;
5940
+ const here = dirname8(fileURLToPath2(import.meta.url));
5941
+ try {
5942
+ const { stdout } = await exec3("git", ["-C", here, "rev-parse", "--path-format=absolute", "--git-common-dir"], { env: withoutGitVars(process.env) });
5943
+ return dirname8(stdout.trim());
5944
+ } catch {
5945
+ return resolve5(here, "..", "..", "..", "..");
5946
+ }
5947
+ }
5948
+ async function locateAgentEnv(env, home, kind2 = installKind()) {
5949
+ return resolveAgentEnvPath({ env, home, checkout: await mainCheckout(kind2), exists: existsSync3 });
5950
+ }
5951
+ function agentEnvFile(path, now = () => /* @__PURE__ */ new Date()) {
5952
+ return {
5953
+ path,
5954
+ read: async () => {
5955
+ try {
5956
+ return await readFile8(path, "utf8");
5957
+ } catch (err) {
5958
+ if (err.code === "ENOENT") {
5959
+ throw new Error(`no agent config at ${path}: set the agent up first (docs/DEPLOY.md), or set TARFA_AGENT_ENV`);
5960
+ }
5961
+ throw err;
5962
+ }
5963
+ },
5964
+ write: async (text) => {
5965
+ let backup = `${path}.bak-${now().toISOString().replace(/[:.]/g, "-")}`;
5966
+ try {
5967
+ await copyFile(path, backup);
5968
+ await chmod2(backup, 384);
5969
+ } catch (err) {
5970
+ if (err.code !== "ENOENT") throw err;
5971
+ backup = null;
5972
+ }
5973
+ await mkdir7(dirname8(path), { recursive: true, mode: 448 });
5974
+ await writeFile5(path, text, { mode: 384 });
5975
+ await chmod2(path, 384);
5976
+ return backup;
5977
+ }
5978
+ };
5936
5979
  }
5937
5980
 
5938
5981
  // src/infrastructure/file-status-store.ts
5982
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
5983
+ import { dirname as dirname9 } from "node:path";
5939
5984
  var FileStatusStore = class {
5940
5985
  constructor(filePath, now = Date.now) {
5941
5986
  this.filePath = filePath;
@@ -6510,6 +6555,7 @@ export {
6510
6555
  browseDir,
6511
6556
  atomicWrite,
6512
6557
  loadOrCreateAgentKeys,
6558
+ readAgentPublicKey,
6513
6559
  enrolmentMode,
6514
6560
  withEnrolmentMode,
6515
6561
  standingOf,
@@ -6517,6 +6563,17 @@ export {
6517
6563
  revokeDevice,
6518
6564
  forgetDevice,
6519
6565
  FileDeviceStore,
6566
+ isWorkspaceStatus,
6567
+ runsClaude,
6568
+ imageMediaType,
6569
+ pdfMediaType,
6570
+ audioMediaType,
6571
+ PROTOCOL_VERSION,
6572
+ AGENT_FEATURES,
6573
+ parseClientMessage,
6574
+ parseBrowserClientMessage,
6575
+ parseBrowserAgentMessage,
6576
+ workspaceLink,
6520
6577
  DICTATION_PROVIDERS,
6521
6578
  providerOf,
6522
6579
  changeDictationKey,
@@ -6531,16 +6588,6 @@ export {
6531
6588
  mainCheckout,
6532
6589
  locateAgentEnv,
6533
6590
  agentEnvFile,
6534
- isWorkspaceStatus,
6535
- runsClaude,
6536
- imageMediaType,
6537
- pdfMediaType,
6538
- audioMediaType,
6539
- PROTOCOL_VERSION,
6540
- AGENT_FEATURES,
6541
- parseClientMessage,
6542
- parseBrowserClientMessage,
6543
- parseBrowserAgentMessage,
6544
6591
  FileStatusStore,
6545
6592
  FileParkedStore,
6546
6593
  FsFileWriter,