@shipers-dev/dayline 0.95.0 → 0.97.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 (2) hide show
  1. package/dist/index.js +204 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -45260,7 +45260,7 @@ import { parseArgs } from "util";
45260
45260
  // package.json
45261
45261
  var package_default = {
45262
45262
  name: "@shipers-dev/dayline",
45263
- version: "0.95.0",
45263
+ version: "0.97.0",
45264
45264
  type: "module",
45265
45265
  bin: {
45266
45266
  dayline: "./dist/index.js"
@@ -46573,6 +46573,7 @@ function buildProjectTree(args2) {
46573
46573
  }
46574
46574
 
46575
46575
  // src/_impl/daemon-main.ts
46576
+ init_paths();
46576
46577
  init_errors();
46577
46578
 
46578
46579
  // src/_impl/funnel.ts
@@ -46673,12 +46674,116 @@ function parseChatMessageBody(raw) {
46673
46674
  return { ok: true, value: { chat_id: chatId, text } };
46674
46675
  }
46675
46676
 
46677
+ // src/_impl/api-proxy.ts
46678
+ //! The daemon's forwarder to the worker API.
46679
+ //!
46680
+ //! Native clients (the desktop app) never reach the worker themselves: every
46681
+ //! call they make rides `/api/**` on the local daemon, so the only host the
46682
+ //! app connects to is 127.0.0.1 and exactly one place — here — decides what
46683
+ //! leaves the machine. Split out of daemon-main so the allowlist is readable
46684
+ //! on its own and testable without standing up a server.
46685
+ var API_PROXY_ROUTES = [
46686
+ { method: "GET", path: "/api/auth/me" },
46687
+ { method: "GET", path: "/api/workspaces/:ws/agents" },
46688
+ { method: "GET", path: "/api/workspaces/:ws/devices" },
46689
+ { method: "GET", path: "/api/workspaces/:ws/skills" },
46690
+ { method: "GET", path: "/api/workspaces/:ws/projects" },
46691
+ { method: "GET", path: "/api/workspaces/:ws/projects/:project/chats" },
46692
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/chats" },
46693
+ { method: "GET", path: "/api/workspaces/:ws/chats/:chat" },
46694
+ { method: "PATCH", path: "/api/workspaces/:ws/chats/:chat" },
46695
+ { method: "DELETE", path: "/api/workspaces/:ws/chats/:chat" },
46696
+ { method: "GET", path: "/api/workspaces/:ws/chats/:chat/snapshot" },
46697
+ { method: "POST", path: "/api/workspaces/:ws/agent/chats/query" },
46698
+ { method: "GET", path: "/api/workspaces/:ws/projects/:project/issues" },
46699
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/issues" },
46700
+ { method: "GET", path: "/api/workspaces/:ws/projects/:project/issues/:issue" },
46701
+ { method: "PATCH", path: "/api/workspaces/:ws/projects/:project/issues/:issue" },
46702
+ { method: "DELETE", path: "/api/workspaces/:ws/projects/:project/issues/:issue" },
46703
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/issues/:issue/complete" },
46704
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/issues/:issue/fail" },
46705
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/issues/:issue/stop" },
46706
+ { method: "GET", path: "/api/workspaces/:ws/projects/:project/issues/:issue/dispatches" },
46707
+ { method: "GET", path: "/api/workspaces/:ws/projects/:project/issues/:issue/activity" },
46708
+ { method: "GET", path: "/api/workspaces/:ws/projects/:project/devices" },
46709
+ { method: "PATCH", path: "/api/workspaces/:ws/projects/:project/devices/:device" },
46710
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/devices/:device/prepare-working-dir" },
46711
+ { method: "POST", path: "/api/workspaces/:ws/projects/:project/devices/:device/worktree" },
46712
+ { method: "GET", path: "/api/devices/:device/fs/*" }
46713
+ ];
46714
+ var API_PROXY_MATCHERS = API_PROXY_ROUTES.map(({ method, path }) => ({
46715
+ method,
46716
+ re: new RegExp(`^${path.split("/").map((seg) => seg.startsWith(":") ? "[^/]+" : seg === "*" ? ".+" : seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}$`)
46717
+ }));
46718
+ function apiProxyAllows(method, pathname) {
46719
+ return API_PROXY_MATCHERS.some((r) => r.method === method && r.re.test(pathname));
46720
+ }
46721
+ var HOP_BY_HOP = new Set([
46722
+ "host",
46723
+ "connection",
46724
+ "keep-alive",
46725
+ "proxy-authenticate",
46726
+ "proxy-authorization",
46727
+ "te",
46728
+ "trailer",
46729
+ "transfer-encoding",
46730
+ "upgrade",
46731
+ "content-length",
46732
+ "authorization",
46733
+ "origin",
46734
+ "referer",
46735
+ "accept-encoding"
46736
+ ]);
46737
+ function makeApiProxy(apiUrl, session) {
46738
+ return async function proxyToApi(req, url2) {
46739
+ const userSession = session();
46740
+ if (!userSession) {
46741
+ return Response.json({
46742
+ ok: false,
46743
+ code: "no_device_session",
46744
+ message: "this device has no user session — re-pair it with `dayline connect`"
46745
+ }, { status: 401 });
46746
+ }
46747
+ const target = new URL(`${apiUrl.replace(/\/+$/, "")}${url2.pathname}`);
46748
+ target.search = url2.search;
46749
+ target.searchParams.delete("token");
46750
+ const headers = new Headers;
46751
+ for (const [k, v] of req.headers) {
46752
+ if (!HOP_BY_HOP.has(k.toLowerCase()))
46753
+ headers.set(k, v);
46754
+ }
46755
+ headers.set("authorization", `Bearer ${userSession}`);
46756
+ const method = req.method.toUpperCase();
46757
+ const body = method === "GET" || method === "HEAD" ? undefined : await req.arrayBuffer();
46758
+ let upstream;
46759
+ try {
46760
+ upstream = await fetch(target.toString(), {
46761
+ method,
46762
+ headers,
46763
+ body,
46764
+ redirect: "manual",
46765
+ signal: AbortSignal.timeout(60000)
46766
+ });
46767
+ } catch (e) {
46768
+ return Response.json({ ok: false, code: "upstream_unreachable", message: String(e?.message || e) }, { status: 502 });
46769
+ }
46770
+ const bytes = await upstream.arrayBuffer();
46771
+ const out = new Headers;
46772
+ for (const name of ["content-type", "cache-control", "etag", "location"]) {
46773
+ const v = upstream.headers.get(name);
46774
+ if (v)
46775
+ out.set(name, v);
46776
+ }
46777
+ return new Response(bytes, { status: upstream.status, headers: out });
46778
+ };
46779
+ }
46780
+
46676
46781
  // src/_impl/daemon-main.ts
46677
46782
  init_lib();
46678
46783
  // package.json
46679
46784
  var package_default2 = {
46680
46785
  name: "@shipers-dev/dayline",
46681
- version: "0.95.0",
46786
+ version: "0.97.0",
46682
46787
  type: "module",
46683
46788
  bin: {
46684
46789
  dayline: "./dist/index.js"
@@ -46943,7 +47048,7 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
46943
47048
  let projectTreeCache = null;
46944
47049
  const corsHeaders = (origin) => ({
46945
47050
  "Access-Control-Allow-Origin": origin || "*",
46946
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
47051
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
46947
47052
  "Access-Control-Allow-Headers": "Authorization, Content-Type",
46948
47053
  "Access-Control-Max-Age": "600",
46949
47054
  Vary: "Origin"
@@ -46959,6 +47064,35 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
46959
47064
  }
46960
47065
  return null;
46961
47066
  };
47067
+ let sessionToken = cfg.sessionToken ?? null;
47068
+ const proxyToApi = makeApiProxy(apiUrl, () => sessionToken);
47069
+ const sessionRoute = async (req) => {
47070
+ if (req.method !== "GET" && req.method !== "DELETE") {
47071
+ return Response.json({ error: "method not allowed" }, { status: 405 });
47072
+ }
47073
+ if (req.method === "DELETE") {
47074
+ sessionToken = null;
47075
+ try {
47076
+ const current = JSON.parse(await Bun.file(CONFIG_PATH).text());
47077
+ delete current.sessionToken;
47078
+ await Bun.write(CONFIG_PATH, JSON.stringify(current, null, 2));
47079
+ } catch (e) {
47080
+ log3(`session: could not persist sign-out: ${e}`);
47081
+ }
47082
+ return Response.json({ signed_in: false });
47083
+ }
47084
+ if (!sessionToken) {
47085
+ return Response.json({ signed_in: false, reason: "no_device_session" });
47086
+ }
47087
+ const me = await proxyToApi(new Request(`http://127.0.0.1/api/auth/me`, { headers: req.headers }), new URL("http://127.0.0.1/api/auth/me"));
47088
+ if (me.status === 401) {
47089
+ return Response.json({ signed_in: false, reason: "session_expired" });
47090
+ }
47091
+ if (!me.ok) {
47092
+ return Response.json({ signed_in: false, reason: "api_unreachable" }, { status: 502 });
47093
+ }
47094
+ return Response.json({ signed_in: true, me: await me.json() });
47095
+ };
46962
47096
  const withCors = (res, origin) => {
46963
47097
  for (const [k, v] of Object.entries(corsHeaders(origin))) {
46964
47098
  res.headers.set(k, v);
@@ -47005,6 +47139,21 @@ var daemonProgram = ({ cfg, apiUrl }) => exports_Effect.gen(function* () {
47005
47139
  const route = () => {
47006
47140
  if (url2.pathname === "/health")
47007
47141
  return Response.json({ ok: true, device_id: cfg.deviceId, cli_version: CLI_VERSION });
47142
+ if (url2.pathname === "/session") {
47143
+ const denied = requireLocalAuth(req, url2, { allowQueryToken: true });
47144
+ if (denied)
47145
+ return denied;
47146
+ return sessionRoute(req);
47147
+ }
47148
+ if (url2.pathname === "/api" || url2.pathname.startsWith("/api/")) {
47149
+ const denied = requireLocalAuth(req, url2, { allowQueryToken: true });
47150
+ if (denied)
47151
+ return denied;
47152
+ if (!apiProxyAllows(req.method.toUpperCase(), url2.pathname)) {
47153
+ return Response.json({ ok: false, code: "route_not_proxied", message: `${req.method} ${url2.pathname} is not a proxied API route` }, { status: 404 });
47154
+ }
47155
+ return proxyToApi(req, url2);
47156
+ }
47008
47157
  if (url2.pathname === "/files" && req.method === "GET") {
47009
47158
  if (req.headers.get("authorization") !== expectedAuth)
47010
47159
  return new Response("unauthorized", { status: 401 });
@@ -48135,7 +48284,8 @@ async function awaitPairing(apiUrl, deviceName, opts = {}) {
48135
48284
  device_id: j.device_id,
48136
48285
  token: j.token,
48137
48286
  dispatch_secret: j.dispatch_secret ?? null,
48138
- workspace_id: j.workspace_id ?? null
48287
+ workspace_id: j.workspace_id ?? null,
48288
+ session: j.session ?? null
48139
48289
  };
48140
48290
  }
48141
48291
  }
@@ -48154,7 +48304,7 @@ var connectCmd = exports_Effect.fn("connectCmd")(function* () {
48154
48304
  const api2 = yield* Api2;
48155
48305
  yield* config2.ensureDirs;
48156
48306
  let cfg = yield* config2.load;
48157
- if (!cfg.deviceId || !cfg.authToken) {
48307
+ if (!cfg.deviceId || !cfg.authToken || !cfg.sessionToken) {
48158
48308
  const apiUrl = cfg.apiUrl || process.env.MULTI_API || process.env.MULTI_API_URL || "https://api.dayline.ai";
48159
48309
  const name = deviceNameFromEnv();
48160
48310
  const detected = yield* exports_Effect.promise(() => detectAgents());
@@ -48171,10 +48321,14 @@ var connectCmd = exports_Effect.fn("connectCmd")(function* () {
48171
48321
  deviceId: bundle.device_id,
48172
48322
  authToken: bundle.token,
48173
48323
  dispatchSecret: bundle.dispatch_secret ?? undefined,
48174
- workspaceId: bundle.workspace_id ?? undefined
48324
+ workspaceId: bundle.workspace_id ?? undefined,
48325
+ sessionToken: bundle.session ?? undefined
48175
48326
  });
48176
48327
  cfg = yield* config2.load;
48177
48328
  yield* logger.log(`connect: paired device=${cfg.deviceId} workspace=${cfg.workspaceId}`);
48329
+ if (!cfg.sessionToken) {
48330
+ yield* logger.log("connect: this API did not issue a device session — desktop clients will stay signed out");
48331
+ }
48178
48332
  }
48179
48333
  if (!cfg.dispatchSecret) {
48180
48334
  return yield* exports_Effect.fail(new UsageError({ message: "Missing dispatch secret. Re-pair via 'dayline setup'." }));
@@ -48214,7 +48368,7 @@ ${err?.stack ?? ""}`);
48214
48368
  // src/commands/service.ts
48215
48369
  init_esm();
48216
48370
  import { existsSync as existsSync18, readFileSync as readFileSync13 } from "fs";
48217
- import { homedir as homedir6, platform } from "os";
48371
+ import { homedir as homedir6, platform, tmpdir } from "os";
48218
48372
  import { join as join19 } from "path";
48219
48373
  init_errors();
48220
48374
  init_paths();
@@ -48242,15 +48396,54 @@ var readPid = () => {
48242
48396
  return null;
48243
48397
  }
48244
48398
  };
48399
+ function isEphemeralBinary(path) {
48400
+ if (!path)
48401
+ return true;
48402
+ return path.includes("/bunx-") || path.includes("/_npx/") || path.startsWith(tmpdir() + "/") || /\/T\/bunx/.test(path);
48403
+ }
48404
+ function pickStableBinary(candidates, exists4) {
48405
+ for (const c of candidates) {
48406
+ if (!c)
48407
+ continue;
48408
+ if (isEphemeralBinary(c))
48409
+ continue;
48410
+ if (exists4(c))
48411
+ return c;
48412
+ }
48413
+ return null;
48414
+ }
48415
+ function stableCandidates(fromWhich) {
48416
+ const home = homedir6();
48417
+ return [
48418
+ fromWhich,
48419
+ join19(home, ".bun/bin/dayline"),
48420
+ "/opt/homebrew/bin/dayline",
48421
+ "/usr/local/bin/dayline",
48422
+ join19(home, ".npm-global/bin/dayline")
48423
+ ];
48424
+ }
48245
48425
  var resolveBinary = exports_Effect.fn("service.resolveBinary")(function* () {
48246
48426
  const proc = Bun.spawn(["which", "dayline"], { stdout: "pipe", stderr: "pipe" });
48247
48427
  yield* exports_Effect.promise(() => proc.exited);
48248
48428
  const out = yield* exports_Effect.promise(() => new Response(proc.stdout).text());
48249
- const path = out.trim();
48250
- if (path && existsSync18(path))
48251
- return path;
48429
+ const fromWhich = out.trim();
48430
+ const stable = pickStableBinary(stableCandidates(fromWhich), existsSync18);
48431
+ if (stable)
48432
+ return stable;
48433
+ console.log("Installing @shipers-dev/dayline globally so the service has a stable binary…");
48434
+ const bun = Bun.which("bun");
48435
+ if (bun) {
48436
+ const add6 = Bun.spawn([bun, "add", "-g", "@shipers-dev/dayline@latest"], {
48437
+ stdout: "inherit",
48438
+ stderr: "inherit"
48439
+ });
48440
+ yield* exports_Effect.promise(() => add6.exited);
48441
+ }
48442
+ const afterInstall = pickStableBinary(stableCandidates(""), existsSync18);
48443
+ if (afterInstall)
48444
+ return afterInstall;
48252
48445
  return yield* exports_Effect.fail(new DaemonError({
48253
- message: "Could not find `dayline` on PATH. Install with: bun install -g @shipers-dev/dayline"
48446
+ message: "Could not find an installed `dayline`. Install it with `bun add -g @shipers-dev/dayline`, then re-run `dayline service install`."
48254
48447
  }));
48255
48448
  });
48256
48449
  var run6 = exports_Effect.fn("service.run")(function* (cmd, args2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/dayline",
3
- "version": "0.95.0",
3
+ "version": "0.97.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dayline": "./dist/index.js"