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

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,3 +1,7 @@
1
+ import {
2
+ getSiteUrl
3
+ } from "./chunk-26VA3ADF.js";
4
+
1
5
  // src/journal.ts
2
6
  import {
3
7
  appendFileSync,
@@ -10,9 +14,18 @@ import {
10
14
  import { homedir } from "os";
11
15
  import { dirname, join } from "path";
12
16
  var JOURNAL_DIR = join(homedir(), ".config", "prim", "moves");
13
- var LEGACY_JOURNAL_PATH = join(JOURNAL_DIR, "journal.ndjson");
14
17
  var UNBOUND_BUCKET = "_unbound";
15
18
  var JOURNAL_BASENAME = "journal.ndjson";
19
+ function envSlug(apiUrl) {
20
+ const slug = apiUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
21
+ if (slug.length === 0 || slug === "." || slug === "..") {
22
+ return "default";
23
+ }
24
+ return slug;
25
+ }
26
+ function currentEnvDir() {
27
+ return join(JOURNAL_DIR, envSlug(getSiteUrl()));
28
+ }
16
29
  var DIR_MODE = 448;
17
30
  var FILE_MODE = 384;
18
31
  var SAFE_BUCKET = /^[A-Za-z0-9_-]+$/;
@@ -27,7 +40,7 @@ function appendMoveToPath(path, move) {
27
40
  }
28
41
  function bucketDir(orgId) {
29
42
  const safe = orgId !== void 0 && SAFE_BUCKET.test(orgId) && !RESERVED_BUCKETS.has(orgId);
30
- return join(JOURNAL_DIR, safe ? orgId : UNBOUND_BUCKET);
43
+ return join(currentEnvDir(), safe ? orgId : UNBOUND_BUCKET);
31
44
  }
32
45
  function journalPath(orgId) {
33
46
  return join(bucketDir(orgId), JOURNAL_BASENAME);
@@ -54,23 +67,63 @@ function readMovesFromPath(path) {
54
67
  }
55
68
  function listBuckets() {
56
69
  const out = [];
57
- if (existsSync(LEGACY_JOURNAL_PATH)) {
58
- out.push({ bucket: "_legacy", path: LEGACY_JOURNAL_PATH });
59
- }
60
- if (!existsSync(JOURNAL_DIR)) {
70
+ const envDir = currentEnvDir();
71
+ if (!existsSync(envDir)) {
61
72
  return out;
62
73
  }
63
- for (const entry of readdirSync(JOURNAL_DIR, { withFileTypes: true })) {
74
+ for (const entry of readdirSync(envDir, { withFileTypes: true })) {
64
75
  if (!entry.isDirectory()) {
65
76
  continue;
66
77
  }
67
- const path = join(JOURNAL_DIR, entry.name, JOURNAL_BASENAME);
78
+ const path = join(envDir, entry.name, JOURNAL_BASENAME);
68
79
  if (existsSync(path)) {
69
80
  out.push({ bucket: entry.name, path });
70
81
  }
71
82
  }
72
83
  return out;
73
84
  }
85
+ var FLUSHING_PREFIX = `${JOURNAL_BASENAME}.flushing.`;
86
+ function parseFlushingPid(name) {
87
+ const segments = name.slice(FLUSHING_PREFIX.length).split(".");
88
+ const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
89
+ return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
90
+ }
91
+ function listFlushingInDir(dir, bucket) {
92
+ if (!existsSync(dir)) {
93
+ return [];
94
+ }
95
+ const out = [];
96
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
97
+ if (!entry.isFile() || !entry.name.startsWith(FLUSHING_PREFIX)) {
98
+ continue;
99
+ }
100
+ const path = join(dir, entry.name);
101
+ const stat = statSync(path);
102
+ const lineCount = readFileSync(path, "utf-8").split("\n").filter((l) => l.length > 0).length;
103
+ out.push({
104
+ bucket,
105
+ path,
106
+ pid: parseFlushingPid(entry.name),
107
+ sizeBytes: stat.size,
108
+ mtimeMs: stat.mtimeMs,
109
+ lineCount
110
+ });
111
+ }
112
+ return out;
113
+ }
114
+ function listFlushing() {
115
+ const envDir = currentEnvDir();
116
+ if (!existsSync(envDir)) {
117
+ return [];
118
+ }
119
+ const out = [];
120
+ for (const entry of readdirSync(envDir, { withFileTypes: true })) {
121
+ if (entry.isDirectory()) {
122
+ out.push(...listFlushingInDir(join(envDir, entry.name), entry.name));
123
+ }
124
+ }
125
+ return out;
126
+ }
74
127
  function bucketStats() {
75
128
  return listBuckets().map(({ bucket, path }) => {
76
129
  const stat = statSync(path);
@@ -193,6 +246,7 @@ export {
193
246
  appendMove,
194
247
  readMovesFromPath,
195
248
  listBuckets,
249
+ listFlushing,
196
250
  bucketStats,
197
251
  SESSIONS_DIR,
198
252
  resolveOrg
@@ -2,10 +2,11 @@
2
2
  import {
3
3
  appendMove,
4
4
  resolveOrg
5
- } from "../chunk-JZGWQDM5.js";
5
+ } from "../chunk-H4OR42TJ.js";
6
6
  import {
7
7
  toCommitMove
8
8
  } from "../chunk-7GHOFNJ2.js";
9
+ import "../chunk-26VA3ADF.js";
9
10
 
10
11
  // src/hooks/post-commit.ts
11
12
  import { execSync, spawn } from "child_process";
@@ -3,9 +3,6 @@ import {
3
3
  bold,
4
4
  color
5
5
  } from "../chunk-4QJOQIY6.js";
6
- import {
7
- getClient
8
- } from "../chunk-26VA3ADF.js";
9
6
  import {
10
7
  scrubFromCwd
11
8
  } from "../chunk-6LAQVM26.js";
@@ -15,6 +12,9 @@ import {
15
12
  import {
16
13
  parseAgent
17
14
  } from "../chunk-7YRBACIE.js";
15
+ import {
16
+ getClient
17
+ } from "../chunk-26VA3ADF.js";
18
18
 
19
19
  // src/hooks/post-tool-use.ts
20
20
  import { readFileSync } from "fs";
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ parseAgent
4
+ } from "../chunk-7YRBACIE.js";
2
5
  import {
3
6
  getClient,
4
7
  getSiteUrl
5
8
  } from "../chunk-26VA3ADF.js";
6
- import {
7
- parseAgent
8
- } from "../chunk-7YRBACIE.js";
9
9
  import {
10
10
  daemonRequest
11
11
  } from "../chunk-UTKQTZHL.js";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  appendMove,
4
4
  resolveOrg
5
- } from "../chunk-JZGWQDM5.js";
5
+ } from "../chunk-H4OR42TJ.js";
6
6
  import {
7
7
  scrubFromCwd
8
8
  } from "../chunk-6LAQVM26.js";
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  parseAgent
15
15
  } from "../chunk-7YRBACIE.js";
16
+ import "../chunk-26VA3ADF.js";
16
17
 
17
18
  // src/hooks/prim-hook.ts
18
19
  import { spawn } from "child_process";
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- getSiteUrl
4
- } from "../chunk-26VA3ADF.js";
5
2
  import {
6
3
  parseAgent
7
4
  } from "../chunk-7YRBACIE.js";
5
+ import {
6
+ getSiteUrl
7
+ } from "../chunk-26VA3ADF.js";
8
8
  import {
9
9
  daemonRequest
10
10
  } from "../chunk-UTKQTZHL.js";
package/dist/index.js CHANGED
@@ -11,6 +11,14 @@ import {
11
11
  daemonOrDirectGet,
12
12
  formatDecisionsWarning
13
13
  } from "./chunk-UWAJCF7P.js";
14
+ import {
15
+ JOURNAL_DIR,
16
+ SESSIONS_DIR,
17
+ bucketStats,
18
+ listBuckets,
19
+ listFlushing,
20
+ readMovesFromPath
21
+ } from "./chunk-H4OR42TJ.js";
14
22
  import {
15
23
  HttpError,
16
24
  REFRESH_TOKEN_PATH,
@@ -22,13 +30,6 @@ import {
22
30
  getTokenExpiresAt,
23
31
  saveTokenExpiry
24
32
  } from "./chunk-26VA3ADF.js";
25
- import {
26
- JOURNAL_DIR,
27
- SESSIONS_DIR,
28
- bucketStats,
29
- listBuckets,
30
- readMovesFromPath
31
- } from "./chunk-JZGWQDM5.js";
32
33
  import {
33
34
  daemonIsLive,
34
35
  daemonRequest
@@ -1882,9 +1883,124 @@ function registerDecisionsCommands(program2) {
1882
1883
  });
1883
1884
  }
1884
1885
 
1886
+ // src/commands/doctor.ts
1887
+ import { existsSync as existsSync5 } from "fs";
1888
+ var DAEMON_PROBE_TIMEOUT_MS = 500;
1889
+ var CONNECTIVITY_TIMEOUT_MS = 3e3;
1890
+ var MS_PER_SECOND = 1e3;
1891
+ var STALE_PENDING_MS = 6e4;
1892
+ var EXIT_UNHEALTHY = 1;
1893
+ function classifyDoctor(checks) {
1894
+ const status = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
1895
+ return {
1896
+ json: { ok: status !== "fail", status, checks },
1897
+ exitCode: status === "fail" ? EXIT_UNHEALTHY : 0
1898
+ };
1899
+ }
1900
+ function checkAuth() {
1901
+ if (!getAuthToken()) {
1902
+ return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
1903
+ }
1904
+ const expiresAt = getTokenExpiresAt();
1905
+ const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
1906
+ if (expiresAt !== void 0 && Date.now() >= expiresAt) {
1907
+ return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
1908
+ name: "auth",
1909
+ status: "fail",
1910
+ detail: "token expired, no refresh \u2014 run `prim auth login`"
1911
+ };
1912
+ }
1913
+ if (!hasRefresh) {
1914
+ return { name: "auth", status: "warn", detail: "no refresh token \u2014 capture stops at expiry" };
1915
+ }
1916
+ const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
1917
+ return { name: "auth", status: "ok", detail };
1918
+ }
1919
+ async function checkDaemon() {
1920
+ const live = await daemonIsLive(DAEMON_PROBE_TIMEOUT_MS);
1921
+ return live ? { name: "daemon", status: "ok", detail: "live" } : {
1922
+ name: "daemon",
1923
+ status: "warn",
1924
+ detail: "down \u2014 capture still journals; drains on next `prim` invocation"
1925
+ };
1926
+ }
1927
+ function checkJournal() {
1928
+ const stats = bucketStats();
1929
+ const pending = stats.reduce((n, s) => n + s.lineCount, 0);
1930
+ if (pending === 0) {
1931
+ return { name: "journal", status: "ok", detail: "no pending moves" };
1932
+ }
1933
+ const oldestMs = Math.max(...stats.map((s) => Date.now() - s.mtimeMs));
1934
+ const oldestS = Math.round(oldestMs / MS_PER_SECOND);
1935
+ if (oldestMs > STALE_PENDING_MS) {
1936
+ return {
1937
+ name: "journal",
1938
+ status: "warn",
1939
+ detail: `${String(pending)} pending, oldest ${String(oldestS)}s \u2014 drain may be stalled`
1940
+ };
1941
+ }
1942
+ return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
1943
+ }
1944
+ function checkStranded() {
1945
+ const stranded = listFlushing();
1946
+ if (stranded.length === 0) {
1947
+ return { name: "stranded", status: "ok", detail: "none" };
1948
+ }
1949
+ const moves = stranded.reduce((n, f) => n + f.lineCount, 0);
1950
+ return {
1951
+ name: "stranded",
1952
+ status: "warn",
1953
+ detail: `${String(moves)} move(s) in ${String(stranded.length)} file(s) \u2014 run \`prim moves flush\``
1954
+ };
1955
+ }
1956
+ async function checkConnectivity() {
1957
+ try {
1958
+ await getClient().get("/api/cli/decisions/recent?limit=1", {
1959
+ signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS)
1960
+ });
1961
+ return { name: "connectivity", status: "ok", detail: "server reachable" };
1962
+ } catch (err) {
1963
+ const message = err instanceof Error ? err.message : String(err);
1964
+ const status = message.includes("Authentication") ? "fail" : "warn";
1965
+ return { name: "connectivity", status, detail: message.slice(0, 80) };
1966
+ }
1967
+ }
1968
+ async function collectChecks() {
1969
+ return [
1970
+ checkAuth(),
1971
+ await checkDaemon(),
1972
+ checkJournal(),
1973
+ checkStranded(),
1974
+ await checkConnectivity()
1975
+ ];
1976
+ }
1977
+ function icon(status) {
1978
+ return status === "ok" ? "\u2713" : status === "warn" ? "\u26A0" : "\u2717";
1979
+ }
1980
+ async function runDoctor() {
1981
+ const checks = await collectChecks();
1982
+ const { json, exitCode } = classifyDoctor(checks);
1983
+ const headline = json.status === "ok" ? "\u2713 healthy" : json.status === "warn" ? "\u26A0 degraded" : "\u2717 unhealthy";
1984
+ process.stderr.write(`[prim] doctor: ${headline}
1985
+ `);
1986
+ for (const c of checks) {
1987
+ process.stderr.write(` ${icon(c.status)} ${c.name.padEnd(13)} ${c.detail}
1988
+ `);
1989
+ }
1990
+ console.log(JSON.stringify(json, null, 2));
1991
+ if (exitCode !== 0 && !process.exitCode) {
1992
+ process.exitCode = exitCode;
1993
+ }
1994
+ }
1995
+ function registerDoctorCommands(program2) {
1996
+ program2.command("doctor").description("Check capture-pipeline health end to end (auth, daemon, journal, server)").action(async () => {
1997
+ await runDoctor();
1998
+ });
1999
+ }
2000
+
1885
2001
  // src/commands/hooks.ts
1886
2002
  import { execSync as execSync2 } from "child_process";
1887
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
2003
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
1888
2004
  import { resolve } from "path";
1889
2005
  import { Option } from "commander";
1890
2006
  var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
@@ -1927,11 +2043,11 @@ function getGitRoot() {
1927
2043
  }
1928
2044
  function detectHusky(gitRoot) {
1929
2045
  const huskyDir = resolve(gitRoot, ".husky");
1930
- if (!existsSync5(huskyDir)) return false;
1931
- if (existsSync5(resolve(huskyDir, "_"))) return true;
1932
- if (existsSync5(resolve(huskyDir, "pre-commit"))) return true;
2046
+ if (!existsSync6(huskyDir)) return false;
2047
+ if (existsSync6(resolve(huskyDir, "_"))) return true;
2048
+ if (existsSync6(resolve(huskyDir, "pre-commit"))) return true;
1933
2049
  const pkgPath = resolve(gitRoot, "package.json");
1934
- if (existsSync5(pkgPath)) {
2050
+ if (existsSync6(pkgPath)) {
1935
2051
  try {
1936
2052
  const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
1937
2053
  const scripts = pkg2.scripts ?? {};
@@ -1960,7 +2076,7 @@ async function askConfirmation(question) {
1960
2076
  }
1961
2077
  function installToHusky(gitRoot, spec = PRE_COMMIT) {
1962
2078
  const hookPath = resolve(gitRoot, ".husky", spec.hookName);
1963
- if (existsSync5(hookPath)) {
2079
+ if (existsSync6(hookPath)) {
1964
2080
  const existing = readFileSync5(hookPath, "utf-8");
1965
2081
  if (containsPrimHook(existing, spec.binName)) {
1966
2082
  console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
@@ -1985,10 +2101,10 @@ ${huskyBlock(spec)}
1985
2101
  function installToDotGit(gitRoot, spec = PRE_COMMIT) {
1986
2102
  const hooksDir = resolve(gitRoot, ".git", "hooks");
1987
2103
  const hookPath = resolve(hooksDir, spec.hookName);
1988
- if (!existsSync5(hooksDir)) {
2104
+ if (!existsSync6(hooksDir)) {
1989
2105
  mkdirSync4(hooksDir, { recursive: true });
1990
2106
  }
1991
- if (existsSync5(hookPath)) {
2107
+ if (existsSync6(hookPath)) {
1992
2108
  const existing = readFileSync5(hookPath, "utf-8");
1993
2109
  if (containsPrimHook(existing, spec.binName)) {
1994
2110
  console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
@@ -2052,7 +2168,7 @@ function registerHooksCommands(program2) {
2052
2168
  const gitRoot = getGitRoot();
2053
2169
  for (const spec of HOOKS) {
2054
2170
  const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
2055
- if (!existsSync5(hookPath)) {
2171
+ if (!existsSync6(hookPath)) {
2056
2172
  console.log(`No ${spec.hookName} hook found.`);
2057
2173
  continue;
2058
2174
  }
@@ -2067,7 +2183,7 @@ function registerHooksCommands(program2) {
2067
2183
  }
2068
2184
 
2069
2185
  // src/commands/moves.ts
2070
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
2186
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
2071
2187
  import { join as join5 } from "path";
2072
2188
 
2073
2189
  // src/flusher.ts
@@ -2075,35 +2191,70 @@ import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
2075
2191
  var BATCH_SIZE = 500;
2076
2192
  var HTTP_TIMEOUT_MS = 1e4;
2077
2193
  var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
2078
- async function drainPath(path) {
2079
- const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
2080
- try {
2081
- renameSync2(path, tmpPath);
2082
- } catch (err) {
2083
- if (err.code === "ENOENT") {
2084
- return 0;
2085
- }
2086
- throw err;
2194
+ var ORPHAN_QUARANTINE_MS = 6e4;
2195
+ function batchMoves(moves, size = BATCH_SIZE) {
2196
+ const batches = [];
2197
+ for (let i = 0; i < moves.length; i += size) {
2198
+ batches.push(moves.slice(i, i + size));
2087
2199
  }
2088
- const moves = readMovesFromPath(tmpPath);
2200
+ return batches;
2201
+ }
2202
+ async function drainFlushingPath(flushingPath) {
2203
+ const moves = readMovesFromPath(flushingPath);
2089
2204
  if (moves.length === 0) {
2090
- unlinkSync3(tmpPath);
2205
+ unlinkSync3(flushingPath);
2091
2206
  return 0;
2092
2207
  }
2093
2208
  const client = getClient();
2094
- for (let i = 0; i < moves.length; i += BATCH_SIZE) {
2095
- const batch = moves.slice(i, i + BATCH_SIZE);
2209
+ for (const batch of batchMoves(moves)) {
2096
2210
  await client.post(
2097
2211
  "/api/cli/moves/ingest",
2098
2212
  { batch },
2099
2213
  { signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
2100
2214
  );
2101
2215
  }
2102
- unlinkSync3(tmpPath);
2216
+ unlinkSync3(flushingPath);
2103
2217
  return moves.length;
2104
2218
  }
2105
- async function flush() {
2219
+ async function drainPath(path) {
2220
+ const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
2221
+ try {
2222
+ renameSync2(path, tmpPath);
2223
+ } catch (err) {
2224
+ if (err.code === "ENOENT") {
2225
+ return 0;
2226
+ }
2227
+ throw err;
2228
+ }
2229
+ return drainFlushingPath(tmpPath);
2230
+ }
2231
+ function processIsAlive2(pid) {
2232
+ try {
2233
+ process.kill(pid, 0);
2234
+ return true;
2235
+ } catch {
2236
+ return false;
2237
+ }
2238
+ }
2239
+ function selectRecoverable(files, now, opts = {}) {
2240
+ const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
2241
+ const isAlive = opts.isAlive ?? processIsAlive2;
2242
+ return files.filter(
2243
+ (f) => f.pid === void 0 ? now - f.mtimeMs > quarantineMs : !isAlive(f.pid)
2244
+ );
2245
+ }
2246
+ async function recoverOrphans() {
2106
2247
  let total = 0;
2248
+ for (const file of selectRecoverable(listFlushing(), Date.now())) {
2249
+ try {
2250
+ total += await drainFlushingPath(file.path);
2251
+ } catch {
2252
+ }
2253
+ }
2254
+ return total;
2255
+ }
2256
+ async function flush() {
2257
+ let total = await recoverOrphans();
2107
2258
  for (const { path } of listBuckets()) {
2108
2259
  total += await drainPath(path);
2109
2260
  }
@@ -2124,7 +2275,7 @@ async function flushIfNeeded() {
2124
2275
  }
2125
2276
 
2126
2277
  // src/commands/moves.ts
2127
- var MS_PER_SECOND = 1e3;
2278
+ var MS_PER_SECOND2 = 1e3;
2128
2279
  var DEFAULT_TAIL_LINES = "20";
2129
2280
  var RADIX_DECIMAL = 10;
2130
2281
  var ID_PREFIX_LEN = 8;
@@ -2142,17 +2293,32 @@ function registerMovesCommands(program2) {
2142
2293
  });
2143
2294
  moves.command("status").description("Show per-bucket pending stats").action(() => {
2144
2295
  const stats = bucketStats();
2145
- if (stats.length === 0) {
2296
+ const stranded = listFlushing();
2297
+ if (stats.length === 0 && stranded.length === 0) {
2146
2298
  console.log("[prim] journal: empty");
2147
2299
  return;
2148
2300
  }
2149
2301
  console.log(`[prim] root: ${JOURNAL_DIR}`);
2150
2302
  for (const s of stats) {
2151
- const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND);
2303
+ const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND2);
2152
2304
  console.log(
2153
2305
  ` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
2154
2306
  );
2155
2307
  }
2308
+ if (stranded.length > 0) {
2309
+ const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
2310
+ const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
2311
+ console.log(
2312
+ `[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
2313
+ );
2314
+ for (const f of stranded) {
2315
+ const ageS = Math.round((Date.now() - f.mtimeMs) / MS_PER_SECOND2);
2316
+ const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
2317
+ console.log(
2318
+ ` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(f.lineCount).padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
2319
+ );
2320
+ }
2321
+ }
2156
2322
  });
2157
2323
  moves.command("tail").description("Pretty-print recent journal entries across all buckets").option("-n, --lines <n>", "number of lines to tail", DEFAULT_TAIL_LINES).action((opts) => {
2158
2324
  const lines = Number.parseInt(opts.lines, RADIX_DECIMAL);
@@ -2178,7 +2344,7 @@ function registerMovesCommands(program2) {
2178
2344
  });
2179
2345
  moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
2180
2346
  const dir = join5(process.cwd(), ".prim");
2181
- if (!existsSync6(dir)) {
2347
+ if (!existsSync7(dir)) {
2182
2348
  mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
2183
2349
  }
2184
2350
  const file = join5(process.cwd(), WORKSPACE_FILE);
@@ -2189,7 +2355,7 @@ function registerMovesCommands(program2) {
2189
2355
  });
2190
2356
  moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
2191
2357
  const file = join5(process.cwd(), WORKSPACE_FILE);
2192
- if (!existsSync6(file)) {
2358
+ if (!existsSync7(file)) {
2193
2359
  console.log("[prim] no workspace binding in cwd");
2194
2360
  return;
2195
2361
  }
@@ -2282,7 +2448,7 @@ function registerReconcileCommands(program2) {
2282
2448
 
2283
2449
  // src/commands/session.ts
2284
2450
  import {
2285
- existsSync as existsSync7,
2451
+ existsSync as existsSync8,
2286
2452
  mkdirSync as mkdirSync6,
2287
2453
  readFileSync as readFileSync6,
2288
2454
  readdirSync,
@@ -2293,7 +2459,7 @@ import { join as join6 } from "path";
2293
2459
  var DIR_MODE2 = 448;
2294
2460
  var FILE_MODE3 = 384;
2295
2461
  function ensureDir() {
2296
- if (!existsSync7(SESSIONS_DIR)) {
2462
+ if (!existsSync8(SESSIONS_DIR)) {
2297
2463
  mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
2298
2464
  }
2299
2465
  }
@@ -2314,7 +2480,7 @@ function registerSessionCommands(program2) {
2314
2480
  console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
2315
2481
  });
2316
2482
  session.command("list").description("List active session markers").action(() => {
2317
- if (!existsSync7(SESSIONS_DIR)) {
2483
+ if (!existsSync8(SESSIONS_DIR)) {
2318
2484
  console.log("[prim] no session markers");
2319
2485
  return;
2320
2486
  }
@@ -2334,7 +2500,7 @@ function registerSessionCommands(program2) {
2334
2500
  });
2335
2501
  session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
2336
2502
  const p = markerPath(sessionId);
2337
- if (!existsSync7(p)) {
2503
+ if (!existsSync8(p)) {
2338
2504
  console.log(`[prim] no marker for session ${sessionId}`);
2339
2505
  return;
2340
2506
  }
@@ -2436,7 +2602,7 @@ function registerSetupCommand(program2) {
2436
2602
  // src/commands/skill.ts
2437
2603
  import {
2438
2604
  closeSync as closeSync3,
2439
- existsSync as existsSync8,
2605
+ existsSync as existsSync9,
2440
2606
  fsyncSync as fsyncSync2,
2441
2607
  openSync as openSync3,
2442
2608
  readFileSync as readFileSync7,
@@ -2461,13 +2627,13 @@ function loadSkill() {
2461
2627
  let dir = __dirname;
2462
2628
  while (dir !== dirname4(dir)) {
2463
2629
  const p = resolve2(dir, "SKILL.md");
2464
- if (existsSync8(p)) return readFileSync7(p, "utf-8");
2630
+ if (existsSync9(p)) return readFileSync7(p, "utf-8");
2465
2631
  dir = dirname4(dir);
2466
2632
  }
2467
2633
  throw new Error("SKILL.md not found in package");
2468
2634
  }
2469
2635
  function detectTargets(cwd) {
2470
- return TARGET_CANDIDATES.filter((p) => existsSync8(resolve2(cwd, p)));
2636
+ return TARGET_CANDIDATES.filter((p) => existsSync9(resolve2(cwd, p)));
2471
2637
  }
2472
2638
  function detectNewline(content) {
2473
2639
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -2516,7 +2682,7 @@ function resolveTarget(cwd, override) {
2516
2682
  function runInstall(cwd, opts) {
2517
2683
  const target = resolveTarget(cwd, opts.target);
2518
2684
  if (target === null) return 1;
2519
- const existing = existsSync8(target) ? readFileSync7(target, "utf-8") : "";
2685
+ const existing = existsSync9(target) ? readFileSync7(target, "utf-8") : "";
2520
2686
  const eol = existing ? detectNewline(existing) : "\n";
2521
2687
  const block = composeBlock(loadSkill(), eol);
2522
2688
  const next = applyBlock(existing, block, eol);
@@ -2535,7 +2701,7 @@ function runInstall(cwd, opts) {
2535
2701
  function runUninstall(cwd, opts) {
2536
2702
  const target = resolveTarget(cwd, opts.target);
2537
2703
  if (target === null) return 1;
2538
- if (!existsSync8(target)) {
2704
+ if (!existsSync9(target)) {
2539
2705
  console.log(`Skill block not present at ${target}`);
2540
2706
  return 0;
2541
2707
  }
@@ -2552,7 +2718,7 @@ function runUninstall(cwd, opts) {
2552
2718
  function runStatus(cwd, opts) {
2553
2719
  const target = resolveTarget(cwd, opts.target);
2554
2720
  if (target === null) return 1;
2555
- const fileExists = existsSync8(target);
2721
+ const fileExists = existsSync9(target);
2556
2722
  let installed = false;
2557
2723
  if (fileExists) {
2558
2724
  const content = readFileSync7(target, "utf-8");
@@ -2787,6 +2953,7 @@ registerDecisionsCommands(program);
2787
2953
  registerClaudeCommands(program);
2788
2954
  registerCodexCommands(program);
2789
2955
  registerDaemonCommands(program);
2956
+ registerDoctorCommands(program);
2790
2957
  registerReconcileCommands(program);
2791
2958
  registerStatuslineCommands(program);
2792
2959
  registerWelcomeCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.29",
3
+ "version": "0.1.0-alpha.31",
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",