@gtc6244/immediacy-daemon 0.1.0

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.
Files changed (133) hide show
  1. package/dist/src/actions.d.ts +22 -0
  2. package/dist/src/actions.js +44 -0
  3. package/dist/src/actions.js.map +1 -0
  4. package/dist/src/agent/agent.d.ts +89 -0
  5. package/dist/src/agent/agent.js +22 -0
  6. package/dist/src/agent/agent.js.map +1 -0
  7. package/dist/src/agent/claude.d.ts +70 -0
  8. package/dist/src/agent/claude.js +404 -0
  9. package/dist/src/agent/claude.js.map +1 -0
  10. package/dist/src/agent/codex.d.ts +44 -0
  11. package/dist/src/agent/codex.js +303 -0
  12. package/dist/src/agent/codex.js.map +1 -0
  13. package/dist/src/agent/mock.d.ts +31 -0
  14. package/dist/src/agent/mock.js +155 -0
  15. package/dist/src/agent/mock.js.map +1 -0
  16. package/dist/src/agent/model-error.d.ts +8 -0
  17. package/dist/src/agent/model-error.js +25 -0
  18. package/dist/src/agent/model-error.js.map +1 -0
  19. package/dist/src/agent/openrouter.d.ts +38 -0
  20. package/dist/src/agent/openrouter.js +60 -0
  21. package/dist/src/agent/openrouter.js.map +1 -0
  22. package/dist/src/agent/workspace-scope.d.ts +25 -0
  23. package/dist/src/agent/workspace-scope.js +71 -0
  24. package/dist/src/agent/workspace-scope.js.map +1 -0
  25. package/dist/src/allowlist.d.ts +56 -0
  26. package/dist/src/allowlist.js +141 -0
  27. package/dist/src/allowlist.js.map +1 -0
  28. package/dist/src/chatlog.d.ts +50 -0
  29. package/dist/src/chatlog.js +179 -0
  30. package/dist/src/chatlog.js.map +1 -0
  31. package/dist/src/cli/paths.d.ts +40 -0
  32. package/dist/src/cli/paths.js +76 -0
  33. package/dist/src/cli/paths.js.map +1 -0
  34. package/dist/src/cli/service.d.ts +20 -0
  35. package/dist/src/cli/service.js +245 -0
  36. package/dist/src/cli/service.js.map +1 -0
  37. package/dist/src/cli.d.ts +2 -0
  38. package/dist/src/cli.js +227 -0
  39. package/dist/src/cli.js.map +1 -0
  40. package/dist/src/config.d.ts +33 -0
  41. package/dist/src/config.js +122 -0
  42. package/dist/src/config.js.map +1 -0
  43. package/dist/src/credentials.d.ts +38 -0
  44. package/dist/src/credentials.js +120 -0
  45. package/dist/src/credentials.js.map +1 -0
  46. package/dist/src/crypto.d.ts +49 -0
  47. package/dist/src/crypto.js +202 -0
  48. package/dist/src/crypto.js.map +1 -0
  49. package/dist/src/daemon-identity.d.ts +31 -0
  50. package/dist/src/daemon-identity.js +97 -0
  51. package/dist/src/daemon-identity.js.map +1 -0
  52. package/dist/src/envelope.d.ts +14 -0
  53. package/dist/src/envelope.js +28 -0
  54. package/dist/src/envelope.js.map +1 -0
  55. package/dist/src/events.d.ts +40 -0
  56. package/dist/src/events.js +24 -0
  57. package/dist/src/events.js.map +1 -0
  58. package/dist/src/forward.d.ts +51 -0
  59. package/dist/src/forward.js +170 -0
  60. package/dist/src/forward.js.map +1 -0
  61. package/dist/src/fs-scope.d.ts +17 -0
  62. package/dist/src/fs-scope.js +47 -0
  63. package/dist/src/fs-scope.js.map +1 -0
  64. package/dist/src/github.d.ts +70 -0
  65. package/dist/src/github.js +178 -0
  66. package/dist/src/github.js.map +1 -0
  67. package/dist/src/gitops.d.ts +113 -0
  68. package/dist/src/gitops.js +557 -0
  69. package/dist/src/gitops.js.map +1 -0
  70. package/dist/src/hub.d.ts +50 -0
  71. package/dist/src/hub.js +92 -0
  72. package/dist/src/hub.js.map +1 -0
  73. package/dist/src/index.d.ts +22 -0
  74. package/dist/src/index.js +56 -0
  75. package/dist/src/index.js.map +1 -0
  76. package/dist/src/jobs/scheduler.d.ts +94 -0
  77. package/dist/src/jobs/scheduler.js +373 -0
  78. package/dist/src/jobs/scheduler.js.map +1 -0
  79. package/dist/src/jobs/store.d.ts +33 -0
  80. package/dist/src/jobs/store.js +111 -0
  81. package/dist/src/jobs/store.js.map +1 -0
  82. package/dist/src/log.d.ts +61 -0
  83. package/dist/src/log.js +166 -0
  84. package/dist/src/log.js.map +1 -0
  85. package/dist/src/migrate-state-dir.d.ts +1 -0
  86. package/dist/src/migrate-state-dir.js +47 -0
  87. package/dist/src/migrate-state-dir.js.map +1 -0
  88. package/dist/src/models.d.ts +36 -0
  89. package/dist/src/models.js +71 -0
  90. package/dist/src/models.js.map +1 -0
  91. package/dist/src/permission-gate.d.ts +48 -0
  92. package/dist/src/permission-gate.js +93 -0
  93. package/dist/src/permission-gate.js.map +1 -0
  94. package/dist/src/permission-scope.d.ts +77 -0
  95. package/dist/src/permission-scope.js +244 -0
  96. package/dist/src/permission-scope.js.map +1 -0
  97. package/dist/src/ports.d.ts +71 -0
  98. package/dist/src/ports.js +216 -0
  99. package/dist/src/ports.js.map +1 -0
  100. package/dist/src/projects.d.ts +83 -0
  101. package/dist/src/projects.js +205 -0
  102. package/dist/src/projects.js.map +1 -0
  103. package/dist/src/protocol.d.ts +1226 -0
  104. package/dist/src/protocol.js +16 -0
  105. package/dist/src/protocol.js.map +1 -0
  106. package/dist/src/repo-sync.d.ts +49 -0
  107. package/dist/src/repo-sync.js +83 -0
  108. package/dist/src/repo-sync.js.map +1 -0
  109. package/dist/src/server.d.ts +33 -0
  110. package/dist/src/server.js +973 -0
  111. package/dist/src/server.js.map +1 -0
  112. package/dist/src/terminal.d.ts +32 -0
  113. package/dist/src/terminal.js +92 -0
  114. package/dist/src/terminal.js.map +1 -0
  115. package/dist/src/titler.d.ts +15 -0
  116. package/dist/src/titler.js +72 -0
  117. package/dist/src/titler.js.map +1 -0
  118. package/dist/src/types.d.ts +99 -0
  119. package/dist/src/types.js +3 -0
  120. package/dist/src/types.js.map +1 -0
  121. package/dist/src/usage-limit.d.ts +49 -0
  122. package/dist/src/usage-limit.js +83 -0
  123. package/dist/src/usage-limit.js.map +1 -0
  124. package/dist/src/usage.d.ts +23 -0
  125. package/dist/src/usage.js +63 -0
  126. package/dist/src/usage.js.map +1 -0
  127. package/dist/src/workspace.d.ts +303 -0
  128. package/dist/src/workspace.js +1086 -0
  129. package/dist/src/workspace.js.map +1 -0
  130. package/dist/src/worktree.d.ts +133 -0
  131. package/dist/src/worktree.js +561 -0
  132. package/dist/src/worktree.js.map +1 -0
  133. package/package.json +67 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * DaemonIdentity — this daemon's stable id + human name.
3
+ *
4
+ * A client can talk to several daemons at once (it dials each tunnel/host
5
+ * independently — daemons never talk to each other), and labels each by name.
6
+ * This store gives the daemon a stable `daemonId` (minted once, persisted) and a
7
+ * human `name` that defaults to the machine hostname and can be changed over the
8
+ * wire (`daemon.rename`). Persisted to `~/.immediacy/daemon.json`.
9
+ *
10
+ * No secret material, so the file is world-readable (0644) like config.json —
11
+ * but it keeps the same serialized atomic-rename persist() so a concurrent
12
+ * rename can't tear the file.
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
16
+ import { readFileSync } from "node:fs";
17
+ import { homedir, hostname } from "node:os";
18
+ import { fileURLToPath } from "node:url";
19
+ import { dirname, join } from "node:path";
20
+ import { PROTOCOL_VERSION } from "./protocol.js";
21
+ const IDENTITY_FILE = process.env.DAEMON_IDENTITY_FILE ??
22
+ join(homedir(), ".immediacy", "daemon.json");
23
+ /** A name a malicious/confused client can't blow up to a multi-KB blob. */
24
+ const MAX_NAME_LEN = 200;
25
+ /** The daemon package version, read once from the repo-root VERSION file. */
26
+ function readVersion() {
27
+ // This module lives at daemon/src/daemon-identity.ts, so ../../VERSION.
28
+ try {
29
+ const p = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "VERSION");
30
+ return readFileSync(p, "utf8").trim() || "unknown";
31
+ }
32
+ catch {
33
+ return "unknown";
34
+ }
35
+ }
36
+ export class DaemonIdentity {
37
+ daemonId = "";
38
+ name = "";
39
+ version = readVersion();
40
+ /** Serializes writes so two concurrent rename() calls can't interleave. */
41
+ writeChain = Promise.resolve();
42
+ /**
43
+ * Load the persisted identity, minting a fresh id (and a hostname-derived
44
+ * default name) on first run and writing it back so it stays stable.
45
+ */
46
+ async load() {
47
+ let stored = {};
48
+ try {
49
+ stored = JSON.parse(await readFile(IDENTITY_FILE, "utf8"));
50
+ }
51
+ catch {
52
+ /* no prior state — first run */
53
+ }
54
+ this.daemonId =
55
+ typeof stored.daemonId === "string" && stored.daemonId.trim()
56
+ ? stored.daemonId
57
+ : randomUUID();
58
+ this.name =
59
+ typeof stored.name === "string" && stored.name.trim()
60
+ ? stored.name.trim().slice(0, MAX_NAME_LEN)
61
+ : hostname();
62
+ // Persist so a freshly minted id/name survives the next restart.
63
+ await this.persist();
64
+ console.log(`[daemon] identity "${this.name}" (${this.daemonId})`);
65
+ }
66
+ /** Set the human name (trimmed, non-empty, length-capped) and persist. */
67
+ async rename(name) {
68
+ const trimmed = name.trim().slice(0, MAX_NAME_LEN);
69
+ if (!trimmed)
70
+ throw new Error("daemon name is empty");
71
+ this.name = trimmed;
72
+ await this.persist();
73
+ }
74
+ /** The wire payload for `daemon.info` (pushed on connect / rename / get). */
75
+ info() {
76
+ return {
77
+ daemonId: this.daemonId,
78
+ name: this.name,
79
+ hostname: hostname(),
80
+ version: this.version,
81
+ protocolVersion: PROTOCOL_VERSION,
82
+ };
83
+ }
84
+ persist() {
85
+ const snapshot = JSON.stringify({ daemonId: this.daemonId, name: this.name }, null, 2);
86
+ this.writeChain = this.writeChain
87
+ .catch(() => { })
88
+ .then(async () => {
89
+ await mkdir(dirname(IDENTITY_FILE), { recursive: true });
90
+ const tmp = `${IDENTITY_FILE}.${process.pid}.tmp`;
91
+ await writeFile(tmp, snapshot);
92
+ await rename(tmp, IDENTITY_FILE);
93
+ });
94
+ return this.writeChain;
95
+ }
96
+ }
97
+ //# sourceMappingURL=daemon-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-identity.js","sourceRoot":"","sources":["../../src/daemon-identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Envelope construction + the shared ProtocolError.
3
+ *
4
+ * The (de)serialization seam lives in `crypto.ts` (`clearCodec` / `boxCodec` →
5
+ * `parseEnvelope`), because whether `payload` is cleartext or a sealed NaCl box
6
+ * is a per-socket codec decision. This module only stamps outbound envelopes.
7
+ */
8
+ import { Envelope, MessageType, PayloadFor } from "./protocol.js";
9
+ /** Build a fully-stamped envelope ready to send. */
10
+ export declare function make<T extends MessageType>(type: T, ws: string | null, payload: PayloadFor<T>): Envelope<T>;
11
+ export declare class ProtocolError extends Error {
12
+ code: string;
13
+ constructor(code: string, message: string);
14
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Envelope construction + the shared ProtocolError.
3
+ *
4
+ * The (de)serialization seam lives in `crypto.ts` (`clearCodec` / `boxCodec` →
5
+ * `parseEnvelope`), because whether `payload` is cleartext or a sealed NaCl box
6
+ * is a per-socket codec decision. This module only stamps outbound envelopes.
7
+ */
8
+ import { randomUUID } from "node:crypto";
9
+ import { PROTOCOL_VERSION, } from "./protocol.js";
10
+ /** Build a fully-stamped envelope ready to send. */
11
+ export function make(type, ws, payload) {
12
+ return {
13
+ v: PROTOCOL_VERSION,
14
+ id: randomUUID(),
15
+ ts: Date.now(),
16
+ ws,
17
+ type,
18
+ payload,
19
+ };
20
+ }
21
+ export class ProtocolError extends Error {
22
+ code;
23
+ constructor(code, message) {
24
+ super(message);
25
+ this.code = code;
26
+ }
27
+ }
28
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../src/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAIL,gBAAgB,GACjB,MAAM,eAAe,CAAC;AAEvB,oDAAoD;AACpD,MAAM,UAAU,IAAI,CAClB,IAAO,EACP,EAAiB,EACjB,OAAsB;IAEtB,OAAO;QACL,CAAC,EAAE,gBAAgB;QACnB,EAAE,EAAE,UAAU,EAAE;QAChB,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;QACd,EAAE;QACF,IAAI;QACJ,OAAO;KACR,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,aAAc,SAAQ,KAAK;IAE7B;IADT,YACS,IAAY,EACnB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,SAAI,GAAJ,IAAI,CAAQ;IAIrB,CAAC;CACF"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Internal system-event bus.
3
+ *
4
+ * The daemon already fans events out to *clients* via the write-only `Hub`
5
+ * (`hub.broadcast`), but nothing in-process can subscribe to "an agent finished"
6
+ * or "a PR was opened". The cron/jobs feature needs exactly that: a reactive
7
+ * ("N minutes after X") trigger has to observe identifiable actions as they
8
+ * happen. This is the single in-process pub/sub seam for those domain events.
9
+ *
10
+ * It is deliberately tiny and decoupled: `WorkspaceManager` is handed one and
11
+ * emits onto it at the chokepoints where an action completes; `JobScheduler`
12
+ * is handed the same instance and subscribes. Neither knows about the other.
13
+ */
14
+ import { EventEmitter } from "node:events";
15
+ /**
16
+ * Identifiable actions a reactive job can key off.
17
+ *
18
+ * `agent.error` is INTERNAL-ONLY: it is not a `JobSystemEvent` (protocol.ts), so
19
+ * no user-defined "after" trigger can select it. The scheduler subscribes to it
20
+ * solely to finalize a dispatched run whose agent turn ended in failure — an
21
+ * errored turn emits no `agent.done`, so without this the run (and any ephemeral
22
+ * worktree) would dangle forever.
23
+ */
24
+ export type SystemEventKind = "agent.done" | "agent.error" | "pr.created" | "issue.created" | "commit" | "push" | "workspace.created";
25
+ export interface SystemEvent {
26
+ kind: SystemEventKind;
27
+ /** The workspace the action happened in. */
28
+ ws: string;
29
+ /**
30
+ * Event-specific detail, interpolated into a reactive job's prompt via
31
+ * `{{key}}` placeholders (e.g. `{{url}}` for pr.created, `{{summary}}` for
32
+ * agent.done). Always strings so templating stays trivial.
33
+ */
34
+ context?: Record<string, string>;
35
+ }
36
+ /** Thin typed wrapper over EventEmitter so call sites stay honest. */
37
+ export declare class SystemEvents extends EventEmitter {
38
+ emitSystem(e: SystemEvent): void;
39
+ onSystem(fn: (e: SystemEvent) => void): this;
40
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Internal system-event bus.
3
+ *
4
+ * The daemon already fans events out to *clients* via the write-only `Hub`
5
+ * (`hub.broadcast`), but nothing in-process can subscribe to "an agent finished"
6
+ * or "a PR was opened". The cron/jobs feature needs exactly that: a reactive
7
+ * ("N minutes after X") trigger has to observe identifiable actions as they
8
+ * happen. This is the single in-process pub/sub seam for those domain events.
9
+ *
10
+ * It is deliberately tiny and decoupled: `WorkspaceManager` is handed one and
11
+ * emits onto it at the chokepoints where an action completes; `JobScheduler`
12
+ * is handed the same instance and subscribes. Neither knows about the other.
13
+ */
14
+ import { EventEmitter } from "node:events";
15
+ /** Thin typed wrapper over EventEmitter so call sites stay honest. */
16
+ export class SystemEvents extends EventEmitter {
17
+ emitSystem(e) {
18
+ this.emit("system", e);
19
+ }
20
+ onSystem(fn) {
21
+ return this.on("system", fn);
22
+ }
23
+ }
24
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgC3C,sEAAsE;AACtE,MAAM,OAAO,YAAa,SAAQ,YAAY;IAC5C,UAAU,CAAC,CAAc;QACvB,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IACzB,CAAC;IACD,QAAQ,CAAC,EAA4B;QACnC,OAAO,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Forwarded TCP streams — the daemon side of dev-server preview.
3
+ *
4
+ * A client (the macOS app) runs a loopback listener; each browser connection it
5
+ * accepts becomes a `forward.open` here. We dial `127.0.0.1:<port>` on the
6
+ * daemon box and pipe bytes in both directions as base64 `forward.data` frames,
7
+ * multiplexed over the one WSS connection by `streamId`. This is a raw TCP
8
+ * bridge (not HTTP-aware), so HTTP, WebSocket HMR, and SSE all pass through
9
+ * unchanged — the dev server sees an ordinary local client.
10
+ *
11
+ * Streams are per-socket, like the terminal PTY: the reply bytes go back to the
12
+ * exact client socket that opened the stream, and every stream that socket owns
13
+ * is torn down when it disconnects (`reapClient`).
14
+ *
15
+ * Security: the daemon opens no inbound port and only ever dials loopback, and
16
+ * only ports the `isOwned` check confirms belong to the named workspace's
17
+ * worktree (see `ports.ts`). An `open` for any other port is refused immediately.
18
+ */
19
+ import type { WebSocket } from "ws";
20
+ import type { Hub } from "./hub.js";
21
+ export declare class ForwardManager {
22
+ private hub;
23
+ /** Gate: is `port` currently a worktree-owned listening port for `ws`? */
24
+ private isOwned;
25
+ private streams;
26
+ constructor(hub: Hub,
27
+ /** Gate: is `port` currently a worktree-owned listening port for `ws`? */
28
+ isOwned: (ws: string, port: number) => boolean);
29
+ /**
30
+ * A client accepted a local connection and wants it bridged. Validates the
31
+ * port against the worktree-owned set, dials loopback, and wires both
32
+ * directions. Any failure closes the stream back to the client with a reason.
33
+ */
34
+ open(client: WebSocket, ws: string, streamId: string, port: number): void;
35
+ /** Pause a stream's source read until its client-bound WS queue drains. */
36
+ private pauseUntilDrained;
37
+ /** A chunk of browser→server bytes for an open stream. */
38
+ data(streamId: string, b64: string): void;
39
+ /** The client closed its end; drop the daemon-side socket (no echo back). */
40
+ close(streamId: string): void;
41
+ /** Tear down every stream owned by a socket that just disconnected. */
42
+ reapClient(client: WebSocket): void;
43
+ /** Refuse before a socket ever existed: just tell the client it's closed. */
44
+ private refuse;
45
+ /**
46
+ * The daemon-side socket ended (remote close, error, or write failure). Notify
47
+ * the client so it closes the matching browser connection, then forget it.
48
+ * Idempotent — a socket 'error' immediately followed by 'close' only fires once.
49
+ */
50
+ private teardown;
51
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Forwarded TCP streams — the daemon side of dev-server preview.
3
+ *
4
+ * A client (the macOS app) runs a loopback listener; each browser connection it
5
+ * accepts becomes a `forward.open` here. We dial `127.0.0.1:<port>` on the
6
+ * daemon box and pipe bytes in both directions as base64 `forward.data` frames,
7
+ * multiplexed over the one WSS connection by `streamId`. This is a raw TCP
8
+ * bridge (not HTTP-aware), so HTTP, WebSocket HMR, and SSE all pass through
9
+ * unchanged — the dev server sees an ordinary local client.
10
+ *
11
+ * Streams are per-socket, like the terminal PTY: the reply bytes go back to the
12
+ * exact client socket that opened the stream, and every stream that socket owns
13
+ * is torn down when it disconnects (`reapClient`).
14
+ *
15
+ * Security: the daemon opens no inbound port and only ever dials loopback, and
16
+ * only ports the `isOwned` check confirms belong to the named workspace's
17
+ * worktree (see `ports.ts`). An `open` for any other port is refused immediately.
18
+ */
19
+ import net from "node:net";
20
+ import { logger } from "./log.js";
21
+ /** Cap on concurrent forwarded streams — a backstop against a runaway client. */
22
+ const MAX_STREAMS = 512;
23
+ /** Refuse dials to anything but a normal user port. */
24
+ const MIN_PORT = 1;
25
+ const MAX_PORT = 65535;
26
+ /**
27
+ * Pause reading the dev server once more than this many bytes are queued to the
28
+ * client over the WS but not yet flushed. Bounds daemon memory when the tunnel
29
+ * is slower than a fast local dev server (large asset / SSE download).
30
+ */
31
+ const CLIENT_HIGH_WATER = 1 << 20; // 1 MiB
32
+ /** Re-check the client-bound queue this often while a stream is paused. */
33
+ const DRAIN_POLL_MS = 25;
34
+ /** Abort a dial that accepts the SYN but never establishes / stalls. */
35
+ const CONNECT_TIMEOUT_MS = 10_000;
36
+ export class ForwardManager {
37
+ hub;
38
+ isOwned;
39
+ streams = new Map();
40
+ constructor(hub,
41
+ /** Gate: is `port` currently a worktree-owned listening port for `ws`? */
42
+ isOwned) {
43
+ this.hub = hub;
44
+ this.isOwned = isOwned;
45
+ }
46
+ /**
47
+ * A client accepted a local connection and wants it bridged. Validates the
48
+ * port against the worktree-owned set, dials loopback, and wires both
49
+ * directions. Any failure closes the stream back to the client with a reason.
50
+ */
51
+ open(client, ws, streamId, port) {
52
+ if (!streamId || this.streams.has(streamId))
53
+ return;
54
+ if (this.streams.size >= MAX_STREAMS) {
55
+ this.refuse(client, streamId, "too many forwarded streams");
56
+ return;
57
+ }
58
+ if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
59
+ this.refuse(client, streamId, "invalid port");
60
+ return;
61
+ }
62
+ if (!this.isOwned(ws, port)) {
63
+ this.refuse(client, streamId, `port ${port} is not an active dev server for this workspace`);
64
+ return;
65
+ }
66
+ const sock = net.connect({ host: "127.0.0.1", port });
67
+ const stream = { sock, client, ws, port };
68
+ this.streams.set(streamId, stream);
69
+ // A dial that never establishes (SYN accepted, connection stalls) would
70
+ // otherwise pin a stream slot until the client disconnects.
71
+ const connectTimer = setTimeout(() => {
72
+ if (!stream.connected)
73
+ this.teardown(streamId, "connect timed out");
74
+ }, CONNECT_TIMEOUT_MS);
75
+ connectTimer.unref?.();
76
+ sock.on("connect", () => {
77
+ stream.connected = true;
78
+ clearTimeout(connectTimer);
79
+ logger.debug("forward", "stream open", { ws, data: { port } });
80
+ });
81
+ sock.on("data", (chunk) => {
82
+ this.hub.sendTo(client, "forward.data", null, {
83
+ streamId,
84
+ data: chunk.toString("base64"),
85
+ });
86
+ // Backpressure on the ACTUAL congested resource: bytes queued to the
87
+ // client over the WS. If the tunnel is slower than the dev server, pause
88
+ // reading the source until that queue drains — bounds daemon memory.
89
+ if (client.bufferedAmount > CLIENT_HIGH_WATER)
90
+ this.pauseUntilDrained(streamId);
91
+ });
92
+ sock.on("error", (err) => this.teardown(streamId, err.message));
93
+ sock.on("close", () => this.teardown(streamId));
94
+ }
95
+ /** Pause a stream's source read until its client-bound WS queue drains. */
96
+ pauseUntilDrained(streamId) {
97
+ const stream = this.streams.get(streamId);
98
+ if (!stream || stream.sock.isPaused())
99
+ return;
100
+ stream.sock.pause();
101
+ const check = () => {
102
+ const s = this.streams.get(streamId);
103
+ if (!s || s.sock.destroyed)
104
+ return; // stream gone — nothing to resume
105
+ if (s.client.bufferedAmount > CLIENT_HIGH_WATER) {
106
+ setTimeout(check, DRAIN_POLL_MS).unref?.();
107
+ }
108
+ else {
109
+ s.sock.resume();
110
+ }
111
+ };
112
+ setTimeout(check, DRAIN_POLL_MS).unref?.();
113
+ }
114
+ /** A chunk of browser→server bytes for an open stream. */
115
+ data(streamId, b64) {
116
+ const stream = this.streams.get(streamId);
117
+ if (!stream || stream.sock.destroyed)
118
+ return;
119
+ try {
120
+ stream.sock.write(Buffer.from(b64, "base64"));
121
+ }
122
+ catch (err) {
123
+ // A synchronous throw is rare (write-after-end) — the async failure path
124
+ // is the 'error' handler → teardown. Log so it isn't silently swallowed.
125
+ logger.debug("forward", "stream write failed", {
126
+ data: { streamId, message: err instanceof Error ? err.message : String(err) },
127
+ });
128
+ this.teardown(streamId, "write failed");
129
+ }
130
+ }
131
+ /** The client closed its end; drop the daemon-side socket (no echo back). */
132
+ close(streamId) {
133
+ const stream = this.streams.get(streamId);
134
+ if (!stream)
135
+ return;
136
+ this.streams.delete(streamId);
137
+ stream.sock.destroy();
138
+ }
139
+ /** Tear down every stream owned by a socket that just disconnected. */
140
+ reapClient(client) {
141
+ for (const [id, stream] of this.streams) {
142
+ if (stream.client === client) {
143
+ this.streams.delete(id);
144
+ stream.sock.destroy();
145
+ }
146
+ }
147
+ }
148
+ /** Refuse before a socket ever existed: just tell the client it's closed. */
149
+ refuse(client, streamId, error) {
150
+ logger.debug("forward", "refused stream", { data: { streamId, error } });
151
+ this.hub.sendTo(client, "forward.close", null, { streamId, error });
152
+ }
153
+ /**
154
+ * The daemon-side socket ended (remote close, error, or write failure). Notify
155
+ * the client so it closes the matching browser connection, then forget it.
156
+ * Idempotent — a socket 'error' immediately followed by 'close' only fires once.
157
+ */
158
+ teardown(streamId, error) {
159
+ const stream = this.streams.get(streamId);
160
+ if (!stream)
161
+ return;
162
+ this.streams.delete(streamId);
163
+ stream.sock.destroy();
164
+ this.hub.sendTo(stream.client, "forward.close", null, {
165
+ streamId,
166
+ ...(error ? { error } : {}),
167
+ });
168
+ }
169
+ }
170
+ //# sourceMappingURL=forward.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forward.js","sourceRoot":"","sources":["../../src/forward.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Filesystem containment check, shared by every path that must confine a target
3
+ * to a worktree: the Claude permission gate (`agent/workspace-scope.ts`) and the
4
+ * in-app file viewer (`worktree.ts`). One implementation so the two can't drift —
5
+ * in particular so both are symlink-safe.
6
+ */
7
+ /**
8
+ * True when `target` (absolute or relative to `root`) resolves inside `root`.
9
+ *
10
+ * Canonicalizes with `realpathSync` so a symlink inside the worktree that points
11
+ * outside it cannot pass: `path.resolve` only normalizes `..`/`.` textually and
12
+ * does NOT follow links, so `<worktree>/link/x` (link → /etc) would otherwise
13
+ * look in-tree. We realpath the deepest existing ancestor of the target (the
14
+ * target itself may not exist yet, e.g. a Write to a new file) and re-check
15
+ * containment against the realpath'd root.
16
+ */
17
+ export declare function isInside(root: string, target: string): boolean;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Filesystem containment check, shared by every path that must confine a target
3
+ * to a worktree: the Claude permission gate (`agent/workspace-scope.ts`) and the
4
+ * in-app file viewer (`worktree.ts`). One implementation so the two can't drift —
5
+ * in particular so both are symlink-safe.
6
+ */
7
+ import { existsSync, realpathSync } from "node:fs";
8
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
9
+ /**
10
+ * True when `target` (absolute or relative to `root`) resolves inside `root`.
11
+ *
12
+ * Canonicalizes with `realpathSync` so a symlink inside the worktree that points
13
+ * outside it cannot pass: `path.resolve` only normalizes `..`/`.` textually and
14
+ * does NOT follow links, so `<worktree>/link/x` (link → /etc) would otherwise
15
+ * look in-tree. We realpath the deepest existing ancestor of the target (the
16
+ * target itself may not exist yet, e.g. a Write to a new file) and re-check
17
+ * containment against the realpath'd root.
18
+ */
19
+ export function isInside(root, target) {
20
+ const base = canonical(resolve(root));
21
+ const absLexical = isAbsolute(target) ? resolve(target) : resolve(resolve(root), target);
22
+ const abs = canonical(absLexical);
23
+ const rel = relative(base, abs);
24
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
25
+ }
26
+ /**
27
+ * Resolve symlinks in the deepest existing prefix of `p`, re-appending the
28
+ * not-yet-existing tail. Returns `p` unchanged if nothing on the path exists.
29
+ */
30
+ function canonical(p) {
31
+ let dir = p;
32
+ const tail = [];
33
+ while (!existsSync(dir)) {
34
+ const parent = dirname(dir);
35
+ if (parent === dir)
36
+ return p; // reached the root without finding anything
37
+ tail.unshift(dir.slice(parent.length + 1));
38
+ dir = parent;
39
+ }
40
+ try {
41
+ return tail.length ? resolve(realpathSync(dir), ...tail) : realpathSync(dir);
42
+ }
43
+ catch {
44
+ return p;
45
+ }
46
+ }
47
+ //# sourceMappingURL=fs-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-scope.js","sourceRoot":"","sources":["../../src/fs-scope.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEnE;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,MAAc;IACnD,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACzF,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAChC,OAAO,GAAG,KAAK,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,CAAS;IAC1B,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,CAAC,CAAC,CAAC,4CAA4C;QAC1E,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3C,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * GitHub integration via the `gh` CLI.
3
+ *
4
+ * Why gh, not a stored token: the daemon runs on the user's Mac where `gh` is
5
+ * already authenticated. Reusing it means the GitHub token never touches the
6
+ * phone, the wire, or this source — it stays in gh's keychain (§ no secrets in
7
+ * source / single-user). The daemon just shells out.
8
+ *
9
+ * Flow: list the user's repos → user picks one → `cloneInto` clones it into the
10
+ * project's managed directory → the normal worktree path forks off that clone.
11
+ */
12
+ import type { GithubRepo, GithubStatusResultPayload, IssueSummary, PullRequestSummary } from "./protocol.js";
13
+ /**
14
+ * Legacy clone cache. New GitHub projects clone into the managed projects root
15
+ * (see projects.ts); this constant survives only so pre-existing project
16
+ * records whose repo path points here are still recognised as cached clones.
17
+ */
18
+ export declare const REPO_CACHE: string;
19
+ /** Is `gh` installed AND logged in? Drives whether the app shows the picker. */
20
+ export declare function status(): Promise<GithubStatusResultPayload>;
21
+ /**
22
+ * List every repo the authenticated user can access, most-recently-pushed
23
+ * first.
24
+ *
25
+ * Why `gh api /user/repos` and not `gh repo list`: `gh repo list` only returns
26
+ * repos the user *owns*, so repos where they're a collaborator or org member
27
+ * never reach the picker. `/user/repos?affiliation=owner,collaborator,
28
+ * organization_member` returns the full set the account has access to. We reshape
29
+ * the REST fields into our `GhRepoJson` shape (jq runs per page; `--paginate`
30
+ * follows Link headers, so output is newline-delimited JSON objects).
31
+ */
32
+ export declare function listRepos(limit?: number): Promise<GithubRepo[]>;
33
+ /**
34
+ * Clone a GitHub repo into `dest` via `gh` (uses gh's auth, so private repos
35
+ * work). The caller owns `dest` — it lives in the managed projects root, so the
36
+ * client never chooses where the clone lands.
37
+ */
38
+ export declare function cloneInto(fullName: string, dest: string): Promise<void>;
39
+ /**
40
+ * List a project's open pull requests, newest first. Runs `gh` with the repo
41
+ * clone as its cwd so it targets that repo's `origin` without us threading the
42
+ * `fullName` through — the same keychain-auth stance as the rest of this file.
43
+ * Returns [] rather than throwing when `gh` is absent/unauthenticated or the
44
+ * project has no GitHub remote, so the picker just shows "no open PRs".
45
+ */
46
+ export declare function listPullRequests(repoPath: string): Promise<PullRequestSummary[]>;
47
+ /**
48
+ * Resolve an open PR's head branch name. Used by the "create worktree from PR"
49
+ * flow to know which ref to check out. Throws (unlike the list calls) because
50
+ * the caller can't proceed without it.
51
+ */
52
+ export declare function prHeadRef(repoPath: string, prNumber: number): Promise<string>;
53
+ /**
54
+ * List a project's open issues (with bodies), newest first. Bodies ride along
55
+ * so the "create worktree from an issue" flow can seed and let the user confirm
56
+ * the new worktree's task without a second round-trip. Best-effort like
57
+ * {@link listPullRequests} — returns [] on any `gh` failure.
58
+ */
59
+ export declare function listIssues(repoPath: string): Promise<IssueSummary[]>;
60
+ /**
61
+ * Open a GitHub issue in the workspace's repo via `gh issue create` and return
62
+ * its URL — the mirror of `prCreate`. The agent's produced plan/document is the
63
+ * `body`. Runs with the worktree as cwd so `gh` targets the right repo.
64
+ */
65
+ export declare function createIssue(repoPath: string, title: string, body: string): Promise<string>;
66
+ /**
67
+ * Best-effort refresh of an existing clone's base branch. Never throws, so a
68
+ * daemon that's offline keeps using the clone it already has.
69
+ */
70
+ export declare function refreshRepo(repoPath: string): Promise<void>;