@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.
@@ -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 valid = phoneAddresses2.length > 0 && phoneAddresses2.length <= MAX_ADDRESSES && phoneAddresses2.every(isMachineUrl);
5413
- return valid ? phoneAddresses2 : void 0;
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 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() }));
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
- // ../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
- ];
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
- // ../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);
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
- // ../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);
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 parseBrowserAgentMessage(raw) {
5935
- return browserAgentMessageSchema.parse(raw);
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
- } from "./chunk-UPCCR4ZA.js";
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
@@ -78,7 +78,7 @@ import {
78
78
  systemPathResolver,
79
79
  withLegacyEnv,
80
80
  writeFileAtomic
81
- } from "./chunk-UPCCR4ZA.js";
81
+ } from "./chunk-4M2ZRGQR.js";
82
82
 
83
83
  // src/serve.ts
84
84
  import { homedir as homedir2 } from "node:os";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tarfa/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Tarfa agent and CLI: serves your Claude Code terminal sessions (tmux workspaces) to the Tarfa phone app.",
5
5
  "repository": {
6
6
  "type": "git",