@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.
- package/dist/src/actions.d.ts +22 -0
- package/dist/src/actions.js +44 -0
- package/dist/src/actions.js.map +1 -0
- package/dist/src/agent/agent.d.ts +89 -0
- package/dist/src/agent/agent.js +22 -0
- package/dist/src/agent/agent.js.map +1 -0
- package/dist/src/agent/claude.d.ts +70 -0
- package/dist/src/agent/claude.js +404 -0
- package/dist/src/agent/claude.js.map +1 -0
- package/dist/src/agent/codex.d.ts +44 -0
- package/dist/src/agent/codex.js +303 -0
- package/dist/src/agent/codex.js.map +1 -0
- package/dist/src/agent/mock.d.ts +31 -0
- package/dist/src/agent/mock.js +155 -0
- package/dist/src/agent/mock.js.map +1 -0
- package/dist/src/agent/model-error.d.ts +8 -0
- package/dist/src/agent/model-error.js +25 -0
- package/dist/src/agent/model-error.js.map +1 -0
- package/dist/src/agent/openrouter.d.ts +38 -0
- package/dist/src/agent/openrouter.js +60 -0
- package/dist/src/agent/openrouter.js.map +1 -0
- package/dist/src/agent/workspace-scope.d.ts +25 -0
- package/dist/src/agent/workspace-scope.js +71 -0
- package/dist/src/agent/workspace-scope.js.map +1 -0
- package/dist/src/allowlist.d.ts +56 -0
- package/dist/src/allowlist.js +141 -0
- package/dist/src/allowlist.js.map +1 -0
- package/dist/src/chatlog.d.ts +50 -0
- package/dist/src/chatlog.js +179 -0
- package/dist/src/chatlog.js.map +1 -0
- package/dist/src/cli/paths.d.ts +40 -0
- package/dist/src/cli/paths.js +76 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/service.d.ts +20 -0
- package/dist/src/cli/service.js +245 -0
- package/dist/src/cli/service.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +227 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +33 -0
- package/dist/src/config.js +122 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/credentials.d.ts +38 -0
- package/dist/src/credentials.js +120 -0
- package/dist/src/credentials.js.map +1 -0
- package/dist/src/crypto.d.ts +49 -0
- package/dist/src/crypto.js +202 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/daemon-identity.d.ts +31 -0
- package/dist/src/daemon-identity.js +97 -0
- package/dist/src/daemon-identity.js.map +1 -0
- package/dist/src/envelope.d.ts +14 -0
- package/dist/src/envelope.js +28 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/events.d.ts +40 -0
- package/dist/src/events.js +24 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/forward.d.ts +51 -0
- package/dist/src/forward.js +170 -0
- package/dist/src/forward.js.map +1 -0
- package/dist/src/fs-scope.d.ts +17 -0
- package/dist/src/fs-scope.js +47 -0
- package/dist/src/fs-scope.js.map +1 -0
- package/dist/src/github.d.ts +70 -0
- package/dist/src/github.js +178 -0
- package/dist/src/github.js.map +1 -0
- package/dist/src/gitops.d.ts +113 -0
- package/dist/src/gitops.js +557 -0
- package/dist/src/gitops.js.map +1 -0
- package/dist/src/hub.d.ts +50 -0
- package/dist/src/hub.js +92 -0
- package/dist/src/hub.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.js +56 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/jobs/scheduler.d.ts +94 -0
- package/dist/src/jobs/scheduler.js +373 -0
- package/dist/src/jobs/scheduler.js.map +1 -0
- package/dist/src/jobs/store.d.ts +33 -0
- package/dist/src/jobs/store.js +111 -0
- package/dist/src/jobs/store.js.map +1 -0
- package/dist/src/log.d.ts +61 -0
- package/dist/src/log.js +166 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/migrate-state-dir.d.ts +1 -0
- package/dist/src/migrate-state-dir.js +47 -0
- package/dist/src/migrate-state-dir.js.map +1 -0
- package/dist/src/models.d.ts +36 -0
- package/dist/src/models.js +71 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/permission-gate.d.ts +48 -0
- package/dist/src/permission-gate.js +93 -0
- package/dist/src/permission-gate.js.map +1 -0
- package/dist/src/permission-scope.d.ts +77 -0
- package/dist/src/permission-scope.js +244 -0
- package/dist/src/permission-scope.js.map +1 -0
- package/dist/src/ports.d.ts +71 -0
- package/dist/src/ports.js +216 -0
- package/dist/src/ports.js.map +1 -0
- package/dist/src/projects.d.ts +83 -0
- package/dist/src/projects.js +205 -0
- package/dist/src/projects.js.map +1 -0
- package/dist/src/protocol.d.ts +1226 -0
- package/dist/src/protocol.js +16 -0
- package/dist/src/protocol.js.map +1 -0
- package/dist/src/repo-sync.d.ts +49 -0
- package/dist/src/repo-sync.js +83 -0
- package/dist/src/repo-sync.js.map +1 -0
- package/dist/src/server.d.ts +33 -0
- package/dist/src/server.js +973 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/terminal.d.ts +32 -0
- package/dist/src/terminal.js +92 -0
- package/dist/src/terminal.js.map +1 -0
- package/dist/src/titler.d.ts +15 -0
- package/dist/src/titler.js +72 -0
- package/dist/src/titler.js.map +1 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/usage-limit.d.ts +49 -0
- package/dist/src/usage-limit.js +83 -0
- package/dist/src/usage-limit.js.map +1 -0
- package/dist/src/usage.d.ts +23 -0
- package/dist/src/usage.js +63 -0
- package/dist/src/usage.js.map +1 -0
- package/dist/src/workspace.d.ts +303 -0
- package/dist/src/workspace.js +1086 -0
- package/dist/src/workspace.js.map +1 -0
- package/dist/src/worktree.d.ts +133 -0
- package/dist/src/worktree.js +561 -0
- package/dist/src/worktree.js.map +1 -0
- 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>;
|