@primitive.ai/prim 0.1.0-alpha.28 → 0.1.0-alpha.29

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
- getClient
2
+ getClient,
3
+ getSiteUrl
3
4
  } from "./chunk-26VA3ADF.js";
4
5
  import {
5
6
  daemonRequest
@@ -9,9 +10,11 @@ import {
9
10
  var DAEMON_HTTP_TIMEOUT_MS = 1e4;
10
11
  var DAEMON_PROBE_TIMEOUT_MS = 250;
11
12
  async function daemonOrDirect(method, params, direct) {
12
- const fromDaemon = await daemonRequest(method, params, {
13
- timeoutMs: DAEMON_PROBE_TIMEOUT_MS
14
- });
13
+ const fromDaemon = await daemonRequest(
14
+ method,
15
+ { ...params, callerEnv: getSiteUrl() },
16
+ { timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
17
+ );
15
18
  if (fromDaemon !== null) {
16
19
  return fromDaemon;
17
20
  }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getClient,
4
+ getSiteUrl,
4
5
  getTokenExpiresAt,
5
6
  refreshToken
6
7
  } from "../chunk-26VA3ADF.js";
@@ -10,6 +11,24 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
10
11
  import { createServer } from "net";
11
12
  import { homedir } from "os";
12
13
  import { join } from "path";
14
+
15
+ // src/daemon/env-binding.ts
16
+ function normalizeApiUrl(url) {
17
+ return url.trim().replace(/\/+$/, "");
18
+ }
19
+ function apiUrlsMatch(a, b) {
20
+ return normalizeApiUrl(a) === normalizeApiUrl(b);
21
+ }
22
+ function isCrossEnv(callerEnv, boundEnv) {
23
+ return typeof callerEnv === "string" && !apiUrlsMatch(callerEnv, boundEnv);
24
+ }
25
+ function assertCallerEnvMatches(callerEnv, boundEnv) {
26
+ if (isCrossEnv(callerEnv, boundEnv)) {
27
+ throw new Error(`env mismatch: daemon is bound to ${boundEnv}, caller targets ${callerEnv}`);
28
+ }
29
+ }
30
+
31
+ // src/daemon/server.ts
13
32
  var CONFIG_DIR = join(homedir(), ".config", "prim");
14
33
  var SOCK_PATH = join(CONFIG_DIR, "sock");
15
34
  var PID_PATH = join(CONFIG_DIR, "daemon.pid");
@@ -94,6 +113,7 @@ async function ensureTokenFresh() {
94
113
  }
95
114
  }
96
115
  async function handleConflictCheck(params) {
116
+ assertCallerEnvMatches(params.callerEnv, getSiteUrl());
97
117
  if (typeof params.file !== "string") {
98
118
  throw new Error("conflict_check requires `file: string`");
99
119
  }
@@ -111,18 +131,31 @@ function assertEndpointPath(path, endpoint) {
111
131
  }
112
132
  }
113
133
  async function proxyGet(params, allowedPrefix) {
134
+ assertCallerEnvMatches(params.callerEnv, getSiteUrl());
114
135
  const path = pathParam(params);
115
136
  assertEndpointPath(path, allowedPrefix);
116
137
  return await client.get(path, { signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
117
138
  }
118
- function handleStatusSnapshot() {
119
- const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
120
- const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
121
- return {
139
+ function handleStatusSnapshot(params) {
140
+ const base = {
122
141
  pid: process.pid,
123
142
  uptimeMs: Date.now() - startedAt,
124
143
  sessionId: activeSessionId,
125
- lastHeartbeatAt,
144
+ lastHeartbeatAt
145
+ };
146
+ if (isCrossEnv(params.callerEnv, getSiteUrl())) {
147
+ return {
148
+ ...base,
149
+ onlineCount: void 0,
150
+ onlineNames: void 0,
151
+ presenceStale: false,
152
+ envMismatch: true
153
+ };
154
+ }
155
+ const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
156
+ const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
157
+ return {
158
+ ...base,
126
159
  // Withhold a frozen count/names once they're no longer fresh; the
127
160
  // statusline shows "presence: stale" rather than a confident, wrong list.
128
161
  onlineCount: presenceFresh ? lastOnlineCount : void 0,
@@ -166,7 +199,7 @@ async function dispatchRequest(req) {
166
199
  return { id, ok: true, result: { ack: true } };
167
200
  }
168
201
  case "status_snapshot":
169
- return { id, ok: true, result: handleStatusSnapshot() };
202
+ return { id, ok: true, result: handleStatusSnapshot(req.params ?? {}) };
170
203
  case "ping":
171
204
  return { id, ok: true, result: { pong: true } };
172
205
  default:
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning
5
- } from "../chunk-E5UZXMZL.js";
5
+ } from "../chunk-UWAJCF7P.js";
6
6
  import "../chunk-26VA3ADF.js";
7
7
  import "../chunk-UTKQTZHL.js";
8
8
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- getClient
3
+ getClient,
4
+ getSiteUrl
4
5
  } from "../chunk-26VA3ADF.js";
5
6
  import {
6
7
  parseAgent
@@ -199,7 +200,9 @@ function emit(output) {
199
200
  async function checkOneFile(file) {
200
201
  const fromDaemon = await daemonRequest(
201
202
  "conflict_check",
202
- { file },
203
+ // callerEnv lets a staging-bound daemon refuse a prod-context gate check
204
+ // (and vice versa) so we fall through to a direct call against our own env.
205
+ { file, callerEnv: getSiteUrl() },
203
206
  { timeoutMs: DAEMON_TIMEOUT_MS }
204
207
  );
205
208
  if (fromDaemon) {
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ getSiteUrl
4
+ } from "../chunk-26VA3ADF.js";
2
5
  import {
3
6
  parseAgent
4
7
  } from "../chunk-7YRBACIE.js";
@@ -71,7 +74,9 @@ async function main() {
71
74
  if (parseAgent(process.argv) === "codex") {
72
75
  const snapshot = await daemonRequest(
73
76
  "status_snapshot",
74
- {},
77
+ // callerEnv: a cross-env daemon withholds onlineCount, so a prod Codex
78
+ // session never gets a staging daemon's team count injected.
79
+ { callerEnv: getSiteUrl() },
75
80
  { timeoutMs: DAEMON_TIMEOUT_MS }
76
81
  );
77
82
  if (snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number") {
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  checkAffectedDecisions,
11
11
  daemonOrDirectGet,
12
12
  formatDecisionsWarning
13
- } from "./chunk-E5UZXMZL.js";
13
+ } from "./chunk-UWAJCF7P.js";
14
14
  import {
15
15
  HttpError,
16
16
  REFRESH_TOKEN_PATH,
@@ -851,7 +851,7 @@ ${line("project", result.project)}`);
851
851
 
852
852
  // src/commands/daemon.ts
853
853
  import { spawn } from "child_process";
854
- import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
854
+ import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
855
855
  import { homedir as homedir3 } from "os";
856
856
  import { join as join4 } from "path";
857
857
 
@@ -871,8 +871,12 @@ function formatTeammates(names, cap) {
871
871
 
872
872
  // src/commands/daemon.ts
873
873
  var DAEMON_BIN = "prim-daemon-server";
874
- var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
875
- var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
874
+ var CONFIG_DIR = join4(homedir3(), ".config", "prim");
875
+ var PID_PATH = join4(CONFIG_DIR, "daemon.pid");
876
+ var SOCK_PATH = join4(CONFIG_DIR, "sock");
877
+ var LOG_PATH = join4(CONFIG_DIR, "daemon.log");
878
+ var CONFIG_DIR_MODE = 448;
879
+ var LOG_FILE_MODE = 384;
876
880
  var STOP_TIMEOUT_MS = 5e3;
877
881
  var STOP_POLL_MS = 100;
878
882
  var STATUS_PROBE_TIMEOUT_MS = 500;
@@ -921,6 +925,12 @@ function spawnDaemon(options) {
921
925
  const file = binFile(DAEMON_BIN);
922
926
  return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
923
927
  }
928
+ function openDaemonLog(configDir = CONFIG_DIR) {
929
+ if (!existsSync4(configDir)) {
930
+ mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
931
+ }
932
+ return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
933
+ }
924
934
  async function waitForReady() {
925
935
  const deadline = Date.now() + READY_TIMEOUT_MS;
926
936
  while (Date.now() < deadline) {
@@ -949,8 +959,20 @@ async function daemonStart(opts) {
949
959
  });
950
960
  return;
951
961
  }
952
- const child = spawnDaemon({ detached: true, stdio: ["ignore", "ignore", "ignore"] });
962
+ let logFd;
963
+ try {
964
+ logFd = openDaemonLog();
965
+ } catch {
966
+ logFd = void 0;
967
+ }
968
+ const child = spawnDaemon({
969
+ detached: true,
970
+ stdio: logFd === void 0 ? ["ignore", "ignore", "ignore"] : ["ignore", logFd, logFd]
971
+ });
953
972
  child.unref();
973
+ if (logFd !== void 0) {
974
+ closeSync2(logFd);
975
+ }
954
976
  const live = await waitForReady();
955
977
  if (live) {
956
978
  const after = readPidfile();
@@ -962,7 +984,7 @@ async function daemonStart(opts) {
962
984
  return;
963
985
  }
964
986
  process.stderr.write(
965
- `[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see its log)
987
+ `[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see ${LOG_PATH})
966
988
  `
967
989
  );
968
990
  console.log(JSON.stringify({ started: false }, null, 2));
@@ -1862,7 +1884,7 @@ function registerDecisionsCommands(program2) {
1862
1884
 
1863
1885
  // src/commands/hooks.ts
1864
1886
  import { execSync as execSync2 } from "child_process";
1865
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1887
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1866
1888
  import { resolve } from "path";
1867
1889
  import { Option } from "commander";
1868
1890
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
@@ -1964,7 +1986,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
1964
1986
  const hooksDir = resolve(gitRoot, ".git", "hooks");
1965
1987
  const hookPath = resolve(hooksDir, spec.hookName);
1966
1988
  if (!existsSync5(hooksDir)) {
1967
- mkdirSync3(hooksDir, { recursive: true });
1989
+ mkdirSync4(hooksDir, { recursive: true });
1968
1990
  }
1969
1991
  if (existsSync5(hookPath)) {
1970
1992
  const existing = readFileSync5(hookPath, "utf-8");
@@ -2045,7 +2067,7 @@ function registerHooksCommands(program2) {
2045
2067
  }
2046
2068
 
2047
2069
  // src/commands/moves.ts
2048
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
2070
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
2049
2071
  import { join as join5 } from "path";
2050
2072
 
2051
2073
  // src/flusher.ts
@@ -2157,7 +2179,7 @@ function registerMovesCommands(program2) {
2157
2179
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
2158
2180
  const dir = join5(process.cwd(), ".prim");
2159
2181
  if (!existsSync6(dir)) {
2160
- mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
2182
+ mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
2161
2183
  }
2162
2184
  const file = join5(process.cwd(), WORKSPACE_FILE);
2163
2185
  writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
@@ -2261,7 +2283,7 @@ function registerReconcileCommands(program2) {
2261
2283
  // src/commands/session.ts
2262
2284
  import {
2263
2285
  existsSync as existsSync7,
2264
- mkdirSync as mkdirSync5,
2286
+ mkdirSync as mkdirSync6,
2265
2287
  readFileSync as readFileSync6,
2266
2288
  readdirSync,
2267
2289
  unlinkSync as unlinkSync5,
@@ -2272,7 +2294,7 @@ var DIR_MODE2 = 448;
2272
2294
  var FILE_MODE3 = 384;
2273
2295
  function ensureDir() {
2274
2296
  if (!existsSync7(SESSIONS_DIR)) {
2275
- mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2297
+ mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2276
2298
  }
2277
2299
  }
2278
2300
  function markerPath(sessionId) {
@@ -2413,10 +2435,10 @@ function registerSetupCommand(program2) {
2413
2435
 
2414
2436
  // src/commands/skill.ts
2415
2437
  import {
2416
- closeSync as closeSync2,
2438
+ closeSync as closeSync3,
2417
2439
  existsSync as existsSync8,
2418
2440
  fsyncSync as fsyncSync2,
2419
- openSync as openSync2,
2441
+ openSync as openSync3,
2420
2442
  readFileSync as readFileSync7,
2421
2443
  renameSync as renameSync3,
2422
2444
  writeFileSync as writeFileSync6
@@ -2474,11 +2496,11 @@ function removeBlock(existing) {
2474
2496
  function atomicWrite2(target, content) {
2475
2497
  const tmp = `${target}.tmp`;
2476
2498
  writeFileSync6(tmp, content);
2477
- const fd = openSync2(tmp, "r+");
2499
+ const fd = openSync3(tmp, "r+");
2478
2500
  try {
2479
2501
  fsyncSync2(fd);
2480
2502
  } finally {
2481
- closeSync2(fd);
2503
+ closeSync3(fd);
2482
2504
  }
2483
2505
  renameSync3(tmp, target);
2484
2506
  }
@@ -2602,13 +2624,18 @@ async function renderStatusline() {
2602
2624
  const version = readPackageVersion();
2603
2625
  const snapshot = await daemonRequest(
2604
2626
  "status_snapshot",
2605
- {},
2627
+ // callerEnv lets the daemon withhold presence when it is bound to a different
2628
+ // deployment than this statusline targets.
2629
+ { callerEnv: getSiteUrl() },
2606
2630
  { timeoutMs: STATUSLINE_TIMEOUT_MS }
2607
2631
  );
2608
2632
  if (!snapshot) {
2609
2633
  debug("daemon snapshot missing");
2610
2634
  return `primitive ${version} (daemon: down)`;
2611
2635
  }
2636
+ if (snapshot.envMismatch) {
2637
+ return `primitive ${version} (daemon: live \xB7 presence: other env)`;
2638
+ }
2612
2639
  if (snapshot.presenceStale) {
2613
2640
  return `primitive ${version} (daemon: live \xB7 presence: stale)`;
2614
2641
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.28",
3
+ "version": "0.1.0-alpha.29",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",