@loompad/cli 1.2.5 → 1.2.7

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 (39) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/adapters/antigravity-cli.js +1 -1
  3. package/dist/adapters/antigravity-cli.js.map +1 -1
  4. package/dist/adapters/index.d.ts +1 -1
  5. package/dist/adapters/index.js +25 -1
  6. package/dist/adapters/index.js.map +1 -1
  7. package/dist/cli/doctor.js +3 -1
  8. package/dist/cli/doctor.js.map +1 -1
  9. package/dist/cli/index.js +309 -11
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/core/orchestra.js +17 -1
  12. package/dist/core/orchestra.js.map +1 -1
  13. package/dist/core/registry.d.ts +5 -0
  14. package/dist/core/registry.js +31 -1
  15. package/dist/core/registry.js.map +1 -1
  16. package/dist/core/review-triage.js +3 -13
  17. package/dist/core/review-triage.js.map +1 -1
  18. package/dist/core/secret-scan.d.ts +14 -0
  19. package/dist/core/secret-scan.js +42 -0
  20. package/dist/core/secret-scan.js.map +1 -0
  21. package/dist/core/shares.d.ts +33 -0
  22. package/dist/core/shares.js +54 -0
  23. package/dist/core/shares.js.map +1 -0
  24. package/dist/core/turn-gate.d.ts +53 -0
  25. package/dist/core/turn-gate.js +126 -0
  26. package/dist/core/turn-gate.js.map +1 -0
  27. package/dist/daemon/client.d.ts +19 -0
  28. package/dist/daemon/client.js +81 -5
  29. package/dist/daemon/client.js.map +1 -1
  30. package/dist/daemon/routes/chats.js +18 -0
  31. package/dist/daemon/routes/chats.js.map +1 -1
  32. package/dist/daemon/routes/public.d.ts +1 -1
  33. package/dist/daemon/routes/public.js +21 -0
  34. package/dist/daemon/routes/public.js.map +1 -1
  35. package/dist/daemon/runner.js +3 -1
  36. package/dist/daemon/runner.js.map +1 -1
  37. package/docs/release-notes/v1.2.6.md +18 -0
  38. package/docs/release-notes/v1.2.7.md +6 -0
  39. package/package.json +1 -1
@@ -1,14 +1,4 @@
1
- /**
2
- * Review Triage: what to read first in a turn's diff.
3
- *
4
- * An agent hands back twelve changed files and you have five minutes. Most of
5
- * them are fine to skim; one deleted a test, one touched the auth middleware,
6
- * one added a dependency. This reads the patch and ranks the files by what a
7
- * reviewer would want to see first — each with the reasons, said plainly.
8
- *
9
- * It flags what the diff shows, never guesses at intent: "a test was removed",
10
- * not "the agent cheated". Pure: the runtime calls it on every turn_diff.
11
- */
1
+ import { SECRET_PATTERNS } from "./secret-scan.js";
12
2
  /** Split a unified diff (git's, plus Loom's new-file patches) into files. */
13
3
  export function splitPatch(patch) {
14
4
  const out = [];
@@ -51,8 +41,8 @@ const MIGRATION = /(^|\/)(migrations?|migrate|schema)\/|\.sql$|schema\.prisma$|(
51
41
  const CONFIG = /(^|\/)(\.env[^/]*|tsconfig[^/]*\.json|\.eslintrc[^/]*|eslint\.config\.[a-z]+|vite\.config\.[a-z]+|next\.config\.[a-z]+|webpack\.config\.[a-z]+|jest\.config\.[a-z]+|vitest\.config\.[a-z]+|\.npmrc|\.gitignore)$/;
52
42
  /** What an added line can do that deserves eyes, said the way a reviewer would. */
53
43
  const RISKY = [
54
- [/-----BEGIN [A-Z ]*PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b|\b(sk|rk|pk)_(live|test)_[0-9a-zA-Z]{16,}|\bsk-[A-Za-z0-9_-]{20,}|\bgh[pousr]_[A-Za-z0-9]{30,}|\bxox[baprs]-[A-Za-z0-9-]{10,}/, "what looks like a secret key"],
55
- [/\b(password|passwd|secret|api[_-]?key|token)\b\s*[:=]\s*["'][^"'\s]{6,}["']/i, "a hard-coded credential"],
44
+ // the same secret patterns `loom orchestrate --apply` and `loom chain` check before applying
45
+ ...SECRET_PATTERNS,
56
46
  [/\beval\s*\(|new Function\s*\(/, "eval of a string"],
57
47
  [/\b(child_process|execSync|spawnSync|os\.system|subprocess\.(call|run|Popen)|shell\s*=\s*True)\b/, "running shell commands"],
58
48
  [/\brm\s+-[a-z]*r[a-z]*f|\bgit\s+push\b.*--force|\bgit\s+reset\s+--hard|\bDROP\s+(TABLE|DATABASE)\b|\bTRUNCATE\s+TABLE\b/i, "a destructive command"],
@@ -1 +1 @@
1
- {"version":3,"file":"review-triage.js","sourceRoot":"","sources":["../../src/core/review-triage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"review-triage.js","sourceRoot":"","sources":["../../src/core/review-triage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkCnD,6EAA6E;AAC7E,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,IAAI,GAAG,GAAoB,IAAI,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,IAAI,EAAE,CAAC;YAAC,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAE,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QACxH,MAAM,IAAI,GAAG,sCAAsC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,IAAI,EAAE,CAAC;YAAC,GAAG,GAAG,IAAI,CAAC;YAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAE,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QACxH,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,IAAI,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC;YAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;aACxD,IAAI,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC;YAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;aACzD,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,SAAS;aAC/D,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;aACxD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,SAAS,GAAG,qGAAqG,CAAC;AACxH,MAAM,SAAS,GAAG,wNAAwN,CAAC;AAC3O,MAAM,IAAI,GAAG,sDAAsD,CAAC;AACpE,MAAM,QAAQ,GAAG,oKAAoK,CAAC;AACtL,MAAM,EAAE,GAAG,wQAAwQ,CAAC;AACpR,MAAM,IAAI,GAAG,iJAAiJ,CAAC;AAC/J,MAAM,SAAS,GAAG,8EAA8E,CAAC;AACjG,MAAM,MAAM,GAAG,kNAAkN,CAAC;AAElO,mFAAmF;AACnF,MAAM,KAAK,GAA4B;IACrC,6FAA6F;IAC7F,GAAG,eAAe;IAClB,CAAC,+BAA+B,EAAE,kBAAkB,CAAC;IACrD,CAAC,iGAAiG,EAAE,wBAAwB,CAAC;IAC7H,CAAC,yHAAyH,EAAE,uBAAuB,CAAC;IACpJ,CAAC,iDAAiD,EAAE,oBAAoB,CAAC;IACzE,CAAC,8GAA8G,EAAE,uBAAuB,CAAC;IACzI,CAAC,sEAAsE,EAAE,uBAAuB,CAAC;IACjG,CAAC,4FAA4F,EAAE,kBAAkB,CAAC;CACnH,CAAC;AAEF,MAAM,YAAY,GAAG,8HAA8H,CAAC;AACpJ,MAAM,SAAS,GAAG,wFAAwF,CAAC;AAE3G,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,QAAkD,EAAE;IACxF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,+FAA+F;IAC/F,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IAClL,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAChC,MAAM,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3E,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAc,EAAE;QACtC,MAAM,OAAO,GAA4B,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,GAAW,EAAE,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7G,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEjC,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,KAAK;YAAE,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC;QAC3I,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC,CAAC,OAAO;gBAAE,GAAG,CAAC,EAAE,EAAE,qBAAqB,CAAC,CAAC;iBACzC,CAAC;gBACJ,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC;oBAAE,GAAG,CAAC,EAAE,EAAE,yBAAyB,CAAC,CAAC;gBACjE,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;gBACjH,IAAI,IAAI,IAAI,CAAC;oBAAE,GAAG,CAAC,EAAE,EAAE,WAAW,IAAI,2BAA2B,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;QACD,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,EAAE,EAAE,4CAA4C,CAAC,CAAC;QAClF,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,GAAG,CAAC,EAAE,EAAE,+BAA+B,CAAC,CAAC;QAC/E,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,EAAE,EAAE,6BAA6B,CAAC,CAAC;QAC5D,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,8CAA8C,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAAE,GAAG,CAAC,EAAE,EAAE,8BAA8B,CAAC,CAAC;QACzL,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,EAAE,EAAE,wBAAwB,CAAC,CAAC;QAC3D,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,EAAE,EAAE,kBAAkB,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QAChD,MAAM,MAAM,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtH,IAAI,MAAM,IAAI,KAAK,IAAI,EAAE,IAAI,CAAC,YAAY;YAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,kDAAkD,CAAC,CAAC;QAEhH,IAAI,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC;QACxF,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;YAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC;YAAC,GAAG,CAAC,CAAC,EAAE,kCAAkC,CAAC,CAAC;QAAC,CAAC;aACtH,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,GAAG,EAAE;YAAE,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACrE,MAAM,KAAK,GAAgB,KAAK,IAAI,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;QACnH,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IAC3G,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACzH,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AAClF,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Secrets in a diff, before it's applied or pushed (#340). The same patterns review triage
3
+ * scores a change by: a key or token pasted into code, a hard-coded credential, or a file
4
+ * that is itself a secret (.env, a private key). Pure: reads a unified diff, returns findings.
5
+ */
6
+ /** Text that looks like a secret, and how to say it. */
7
+ export declare const SECRET_PATTERNS: Array<[RegExp, string]>;
8
+ export interface SecretFinding {
9
+ file: string;
10
+ /** Line in the new file, when it's an added line. */
11
+ line?: number;
12
+ what: string;
13
+ }
14
+ export declare function findSecrets(diff: string): SecretFinding[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Secrets in a diff, before it's applied or pushed (#340). The same patterns review triage
3
+ * scores a change by: a key or token pasted into code, a hard-coded credential, or a file
4
+ * that is itself a secret (.env, a private key). Pure: reads a unified diff, returns findings.
5
+ */
6
+ /** Text that looks like a secret, and how to say it. */
7
+ export const SECRET_PATTERNS = [
8
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b|\b(sk|rk|pk)_(live|test)_[0-9a-zA-Z]{16,}|\bsk-[A-Za-z0-9_-]{20,}|\bgh[pousr]_[A-Za-z0-9]{30,}|\bxox[baprs]-[A-Za-z0-9-]{10,}/, "what looks like a secret key"],
9
+ [/\b(password|passwd|secret|api[_-]?key|token)\b\s*[:=]\s*["'][^"'\s]{6,}["']/i, "a hard-coded credential"],
10
+ ];
11
+ /** Files that are secrets by name, whatever they hold. */
12
+ const SECRET_FILE = /(^|\/)(\.env(\.[\w-]+)?|id_(rsa|ed25519|ecdsa)|[^/]+\.(pem|p12|pfx|key))$/i;
13
+ /** …except the ones meant to be committed. */
14
+ const SAFE_FILE = /(^|\/)\.env\.(example|sample|template|defaults)$/i;
15
+ export function findSecrets(diff) {
16
+ const out = [];
17
+ let file = "";
18
+ let line = 0;
19
+ for (const raw of diff.split("\n")) {
20
+ if (raw.startsWith("+++ ")) {
21
+ file = raw.slice(4).replace(/^b\//, "").trim();
22
+ if (file !== "/dev/null" && SECRET_FILE.test(file) && !SAFE_FILE.test(file))
23
+ out.push({ file, what: "a file that holds secrets by its name" });
24
+ continue;
25
+ }
26
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(raw);
27
+ if (hunk) {
28
+ line = Number(hunk[1]);
29
+ continue;
30
+ }
31
+ if (raw.startsWith("+") && !raw.startsWith("+++")) {
32
+ for (const [re, what] of SECRET_PATTERNS)
33
+ if (re.test(raw.slice(1)))
34
+ out.push({ file, line, what });
35
+ line++;
36
+ }
37
+ else if (!raw.startsWith("-"))
38
+ line++;
39
+ }
40
+ return out;
41
+ }
42
+ //# sourceMappingURL=secret-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-scan.js","sourceRoot":"","sources":["../../src/core/secret-scan.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wDAAwD;AACxD,MAAM,CAAC,MAAM,eAAe,GAA4B;IACtD,CAAC,uLAAuL,EAAE,8BAA8B,CAAC;IACzN,CAAC,8EAA8E,EAAE,yBAAyB,CAAC;CAC5G,CAAC;AAEF,0DAA0D;AAC1D,MAAM,WAAW,GAAG,4EAA4E,CAAC;AACjG,8CAA8C;AAC9C,MAAM,SAAS,GAAG,mDAAmD,CAAC;AAStE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,GAAG,GAAoB,EAAE,CAAC;IAChC,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC/C,IAAI,IAAI,KAAK,WAAW,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,uCAAuC,EAAE,CAAC,CAAC;YAC/I,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACnD,IAAI,IAAI,EAAE,CAAC;YAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAAC,SAAS;QAAC,CAAC;QAC/C,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAClD,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,eAAe;gBAAE,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACpG,IAAI,EAAE,CAAC;QACT,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,IAAI,EAAE,CAAC;IAC1C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Read-only thread links (#199): a teammate opens a link and reads a chat — prompts, replies,
3
+ * what ran, what changed — as the replay page (core/replay.ts), without pairing or a token of
4
+ * their own. Each link is a random token that expires and can be revoked; only its SHA-256 is
5
+ * kept on disk, so the shares file alone opens nothing. ~/.loom/shares.json.
6
+ */
7
+ export interface Share {
8
+ /** Short id to list and revoke by (not the token). */
9
+ id: string;
10
+ /** sha256 of the token. */
11
+ hash: string;
12
+ projectId: string;
13
+ chat: string;
14
+ title: string;
15
+ createdAt: number;
16
+ expiresAt: number;
17
+ }
18
+ export declare const MAX_SHARE_HOURS: number;
19
+ /** A new link for one chat, good for `hours` (1 h to 30 days). The token is returned once. */
20
+ export declare function createShare(input: {
21
+ projectId: string;
22
+ chat: string;
23
+ title: string;
24
+ hours?: number;
25
+ }, now?: number): {
26
+ share: Share;
27
+ token: string;
28
+ };
29
+ /** The share a token opens — null when there's none, or it expired or was revoked. */
30
+ export declare function openShare(token: string, now?: number): Share | null;
31
+ export declare function listShares(now?: number): Share[];
32
+ /** Revoke by id; true when there was one. */
33
+ export declare function revokeShare(id: string): boolean;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Read-only thread links (#199): a teammate opens a link and reads a chat — prompts, replies,
3
+ * what ran, what changed — as the replay page (core/replay.ts), without pairing or a token of
4
+ * their own. Each link is a random token that expires and can be revoked; only its SHA-256 is
5
+ * kept on disk, so the shares file alone opens nothing. ~/.loom/shares.json.
6
+ */
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { loomHome } from "./registry.js";
11
+ export const MAX_SHARE_HOURS = 24 * 30;
12
+ const file = () => path.join(loomHome(), "shares.json");
13
+ const sha = (token) => createHash("sha256").update(token).digest("hex");
14
+ function load() {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(file(), "utf8")).filter((s) => s && s.hash);
17
+ }
18
+ catch {
19
+ return [];
20
+ }
21
+ }
22
+ function save(shares) {
23
+ fs.mkdirSync(path.dirname(file()), { recursive: true });
24
+ fs.writeFileSync(file(), JSON.stringify(shares, null, 2) + "\n", { mode: 0o600 });
25
+ }
26
+ /** A new link for one chat, good for `hours` (1 h to 30 days). The token is returned once. */
27
+ export function createShare(input, now = Date.now()) {
28
+ const hours = Math.min(MAX_SHARE_HOURS, Math.max(1, Math.round(input.hours ?? 24)));
29
+ const token = randomBytes(24).toString("base64url");
30
+ const share = { id: randomBytes(4).toString("hex"), hash: sha(token), projectId: input.projectId, chat: input.chat,
31
+ title: input.title, createdAt: now, expiresAt: now + hours * 3_600_000 };
32
+ save([...load().filter((s) => s.expiresAt > now), share]);
33
+ return { share, token };
34
+ }
35
+ /** The share a token opens — null when there's none, or it expired or was revoked. */
36
+ export function openShare(token, now = Date.now()) {
37
+ if (!/^[A-Za-z0-9_-]{20,64}$/.test(token))
38
+ return null;
39
+ const h = sha(token);
40
+ return load().find((s) => s.hash === h && s.expiresAt > now) ?? null;
41
+ }
42
+ export function listShares(now = Date.now()) {
43
+ return load().filter((s) => s.expiresAt > now).sort((a, b) => b.createdAt - a.createdAt);
44
+ }
45
+ /** Revoke by id; true when there was one. */
46
+ export function revokeShare(id) {
47
+ const all = load();
48
+ const left = all.filter((s) => s.id !== id);
49
+ if (left.length === all.length)
50
+ return false;
51
+ save(left);
52
+ return true;
53
+ }
54
+ //# sourceMappingURL=shares.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shares.js","sourceRoot":"","sources":["../../src/core/shares.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The daemon-wide turn gate (#339): how many agent turns may run at once across every
3
+ * project, and how much memory must be free before another starts. Six turns at once —
4
+ * each starting a dev server and headless Chrome — exhausted 16 GB and reset the Mac;
5
+ * past the cap, a turn waits for a slot instead of piling on.
6
+ *
7
+ * Limits live in ~/.loom/limits.json (`loom limits` sets them) and are re-read every few
8
+ * seconds, so a change applies without a restart. A turn never waits forever: some turns
9
+ * wait on others (a parent on its subtask), so after MAX_WAIT_MS it goes ahead and says so.
10
+ */
11
+ export interface TurnLimits {
12
+ /** Turns at once across all projects; 0 = no cap. */
13
+ maxTurns: number;
14
+ /** Don't start a turn while less than this % of memory is available; 0 = no memory gate. */
15
+ minFreePct: number;
16
+ }
17
+ /** One turn per 4 GB (at least 2), and 15% of memory kept free. */
18
+ export declare function defaultLimits(totalBytes?: number): TurnLimits;
19
+ export declare const limitsFile: () => string;
20
+ export declare function readLimits(): TurnLimits;
21
+ export declare function writeLimits(patch: Partial<TurnLimits>): TurnLimits;
22
+ /**
23
+ * % of memory available to start something new. On macOS os.freemem() counts only pages
24
+ * nobody has touched (a few % on a busy Mac), so read vm_stat: free + inactive + speculative
25
+ * + purgeable pages are what the kernel hands out without swapping.
26
+ */
27
+ export declare function availableMemoryPct(): number;
28
+ export interface GateDeps {
29
+ limits(): TurnLimits;
30
+ freePct(): number;
31
+ now(): number;
32
+ sleep(ms: number): Promise<void>;
33
+ }
34
+ /** A counting gate with a memory check; one per daemon process (turnGate below). */
35
+ export declare class TurnGate {
36
+ private deps;
37
+ private running;
38
+ private seq;
39
+ private cache;
40
+ constructor(deps?: GateDeps);
41
+ private limits;
42
+ /** What's running now (labels), for status. */
43
+ active(): string[];
44
+ /** Why a turn can't start right now, or null when it can. */
45
+ private blocker;
46
+ /**
47
+ * Wait for a slot; resolves with release(). `onWait` hears why it's waiting (once, and
48
+ * again if the reason changes), so the thread can say "waiting for a free slot".
49
+ */
50
+ acquire(label: string, onWait?: (why: string) => void): Promise<() => void>;
51
+ }
52
+ /** The daemon's gate: every adapter turn passes through it (adapters/index.ts). */
53
+ export declare const turnGate: TurnGate;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The daemon-wide turn gate (#339): how many agent turns may run at once across every
3
+ * project, and how much memory must be free before another starts. Six turns at once —
4
+ * each starting a dev server and headless Chrome — exhausted 16 GB and reset the Mac;
5
+ * past the cap, a turn waits for a slot instead of piling on.
6
+ *
7
+ * Limits live in ~/.loom/limits.json (`loom limits` sets them) and are re-read every few
8
+ * seconds, so a change applies without a restart. A turn never waits forever: some turns
9
+ * wait on others (a parent on its subtask), so after MAX_WAIT_MS it goes ahead and says so.
10
+ */
11
+ import { execFileSync } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { loomHome } from "./registry.js";
16
+ /** One turn per 4 GB (at least 2), and 15% of memory kept free. */
17
+ export function defaultLimits(totalBytes = os.totalmem()) {
18
+ return { maxTurns: Math.max(2, Math.floor(totalBytes / 4 / 1024 ** 3)), minFreePct: 15 };
19
+ }
20
+ export const limitsFile = () => path.join(loomHome(), "limits.json");
21
+ export function readLimits() {
22
+ const base = defaultLimits();
23
+ try {
24
+ const raw = JSON.parse(fs.readFileSync(limitsFile(), "utf8"));
25
+ return {
26
+ maxTurns: Number.isFinite(raw.maxTurns) && raw.maxTurns >= 0 ? Math.floor(raw.maxTurns) : base.maxTurns,
27
+ minFreePct: Number.isFinite(raw.minFreePct) && raw.minFreePct >= 0 && raw.minFreePct < 100 ? raw.minFreePct : base.minFreePct,
28
+ };
29
+ }
30
+ catch {
31
+ return base;
32
+ }
33
+ }
34
+ export function writeLimits(patch) {
35
+ const next = { ...readLimits(), ...patch };
36
+ fs.mkdirSync(path.dirname(limitsFile()), { recursive: true });
37
+ fs.writeFileSync(limitsFile(), JSON.stringify(next, null, 2) + "\n");
38
+ return next;
39
+ }
40
+ /**
41
+ * % of memory available to start something new. On macOS os.freemem() counts only pages
42
+ * nobody has touched (a few % on a busy Mac), so read vm_stat: free + inactive + speculative
43
+ * + purgeable pages are what the kernel hands out without swapping.
44
+ */
45
+ export function availableMemoryPct() {
46
+ if (process.platform === "darwin") {
47
+ try {
48
+ const out = execFileSync("vm_stat", { encoding: "utf8", timeout: 2000 });
49
+ const size = Number(/page size of (\d+) bytes/.exec(out)?.[1] ?? 16384);
50
+ const pages = (name) => Number(new RegExp(`Pages ${name}:\\s+(\\d+)`).exec(out)?.[1] ?? 0);
51
+ const free = (pages("free") + pages("inactive") + pages("speculative") + pages("purgeable")) * size;
52
+ return Math.min(100, (free / os.totalmem()) * 100);
53
+ }
54
+ catch { /* fall through */ }
55
+ }
56
+ return (os.freemem() / os.totalmem()) * 100;
57
+ }
58
+ const MAX_WAIT_MS = Number(process.env.LOOM_TURN_GATE_MAX_WAIT_MS) || 10 * 60_000;
59
+ const POLL_MS = Number(process.env.LOOM_TURN_GATE_POLL_MS) || 1000;
60
+ /** A counting gate with a memory check; one per daemon process (turnGate below). */
61
+ export class TurnGate {
62
+ deps;
63
+ running = new Set();
64
+ seq = 0;
65
+ cache = null;
66
+ constructor(deps = {
67
+ limits: readLimits, freePct: availableMemoryPct, now: Date.now, sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
68
+ }) {
69
+ this.deps = deps;
70
+ }
71
+ limits() {
72
+ const now = this.deps.now();
73
+ if (!this.cache || now - this.cache.at > 5000)
74
+ this.cache = { at: now, limits: this.deps.limits() };
75
+ return this.cache.limits;
76
+ }
77
+ /** What's running now (labels), for status. */
78
+ active() {
79
+ return [...this.running].map((k) => k.slice(k.indexOf(":") + 1));
80
+ }
81
+ /** Why a turn can't start right now, or null when it can. */
82
+ blocker() {
83
+ const { maxTurns, minFreePct } = this.limits();
84
+ if (maxTurns > 0 && this.running.size >= maxTurns)
85
+ return `${this.running.size} turns are running (the cap is ${maxTurns})`;
86
+ if (minFreePct > 0) {
87
+ const free = this.deps.freePct();
88
+ // the first turn always runs: a gate that blocks everything on a full machine is a hang
89
+ if (free < minFreePct && this.running.size > 0)
90
+ return `only ${Math.round(free)}% of memory is free (Loom keeps ${minFreePct}%)`;
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * Wait for a slot; resolves with release(). `onWait` hears why it's waiting (once, and
96
+ * again if the reason changes), so the thread can say "waiting for a free slot".
97
+ */
98
+ async acquire(label, onWait) {
99
+ const started = this.deps.now();
100
+ let said = null;
101
+ for (;;) {
102
+ const why = this.blocker();
103
+ if (!why)
104
+ break;
105
+ if (this.deps.now() - started > MAX_WAIT_MS) {
106
+ onWait?.(`waited ${Math.round(MAX_WAIT_MS / 60_000)} min for a free slot — starting anyway`);
107
+ break;
108
+ }
109
+ if (why.replace(/\d+%/, "") !== said?.replace(/\d+%/, "")) {
110
+ said = why;
111
+ onWait?.(why);
112
+ }
113
+ await this.deps.sleep(POLL_MS);
114
+ }
115
+ const key = `${++this.seq}:${label}`;
116
+ this.running.add(key);
117
+ let released = false;
118
+ return () => { if (!released) {
119
+ released = true;
120
+ this.running.delete(key);
121
+ } };
122
+ }
123
+ }
124
+ /** The daemon's gate: every adapter turn passes through it (adapters/index.ts). */
125
+ export const turnGate = new TurnGate();
126
+ //# sourceMappingURL=turn-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-gate.js","sourceRoot":"","sources":["../../src/core/turn-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AASzC,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAAC,UAAU,GAAG,EAAE,CAAC,QAAQ,EAAE;IACtD,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;AAC3F,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,aAAa,CAAC,CAAC;AAErE,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,MAAM,CAAC,CAAwB,CAAC;QACrF,OAAO;YACL,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAS,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ;YACzG,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,UAAW,IAAI,CAAC,IAAI,GAAG,CAAC,UAAW,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,UAAW,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU;SACjI,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAA0B;IACpD,MAAM,IAAI,GAAG,EAAE,GAAG,UAAU,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC;IAC3C,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9D,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACzE,MAAM,IAAI,GAAG,MAAM,CAAC,0BAA0B,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;YACxE,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,IAAI,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YACnG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,IAAI,CAAC;YACpG,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,CAAC;AAC9C,CAAC;AAED,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC;AAClF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,IAAI,IAAI,CAAC;AASnE,oFAAoF;AACpF,MAAM,OAAO,QAAQ;IAKC;IAJZ,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAC5B,GAAG,GAAG,CAAC,CAAC;IACR,KAAK,GAA8C,IAAI,CAAC;IAEhE,YAAoB,OAAiB;QACnC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;KACrH;QAFmB,SAAI,GAAJ,IAAI,CAEvB;IAAG,CAAC;IAEG,MAAM;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI;YAAE,IAAI,CAAC,KAAK,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QACpG,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED,+CAA+C;IAC/C,MAAM;QACJ,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,6DAA6D;IACrD,OAAO;QACb,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC/C,IAAI,QAAQ,GAAG,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,QAAQ;YAAE,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,kCAAkC,QAAQ,GAAG,CAAC;QAC5H,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACjC,wFAAwF;YACxF,IAAI,IAAI,GAAG,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC;gBAAE,OAAO,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,mCAAmC,UAAU,IAAI,CAAC;QACnI,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,KAAa,EAAE,MAA8B;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QAChC,IAAI,IAAI,GAAkB,IAAI,CAAC;QAC/B,SAAS,CAAC;YACR,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,GAAG;gBAAE,MAAM;YAChB,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,WAAW,EAAE,CAAC;gBAC5C,MAAM,EAAE,CAAC,UAAU,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,wCAAwC,CAAC,CAAC;gBAC7F,MAAM;YACR,CAAC;YACD,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;gBAAC,IAAI,GAAG,GAAG,CAAC;gBAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;YAAC,CAAC;YACzF,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,EAAE,IAAI,CAAC,GAAG,IAAI,KAAK,EAAE,CAAC;QACrC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,QAAQ,GAAG,KAAK,CAAC;QACrB,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAAC,QAAQ,GAAG,IAAI,CAAC;YAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAAC,CAAC,CAAC,CAAC,CAAC;IACjF,CAAC;CACF;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC"}
@@ -730,6 +730,25 @@ export declare class DaemonClient {
730
730
  finding: import("../core/review.js").Finding;
731
731
  decision: string;
732
732
  }>;
733
+ shareChat(id: string, chat: string, hours?: number): Promise<{
734
+ id: string;
735
+ url: string;
736
+ expiresAt: number;
737
+ title: string;
738
+ }>;
739
+ shares(): Promise<{
740
+ shares: Array<{
741
+ id: string;
742
+ projectId: string;
743
+ chat: string;
744
+ title: string;
745
+ createdAt: number;
746
+ expiresAt: number;
747
+ }>;
748
+ }>;
749
+ revokeShare(shareId: string): Promise<{
750
+ revoked: boolean;
751
+ }>;
733
752
  setAgentPermissions(id: string, agentId: string, permissions: string): Promise<{
734
753
  agent: {
735
754
  permissions?: string;
@@ -3,9 +3,11 @@
3
3
  * Also owns daemon lifecycle: autostart-on-demand, health, shutdown.
4
4
  */
5
5
  import { spawn } from "../core/process.js";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
6
8
  import { fileURLToPath } from "node:url";
7
9
  import WebSocket from "ws";
8
- import { readDaemonConfig, } from "../core/registry.js";
10
+ import { loomHome, readDaemonConfig, } from "../core/registry.js";
9
11
  import { BUILD_REV } from "./server.js";
10
12
  export class DaemonError extends Error {
11
13
  status;
@@ -492,6 +494,15 @@ export class DaemonClient {
492
494
  decideFinding(id, findingId, decision, note) {
493
495
  return this.request("POST", `/api/projects/${encodeURIComponent(id)}/findings/${encodeURIComponent(findingId)}`, { decision, ...(note ? { note } : {}) });
494
496
  }
497
+ shareChat(id, chat, hours) {
498
+ return this.request("POST", `/api/projects/${encodeURIComponent(id)}/chats/${encodeURIComponent(chat)}/share`, hours ? { hours } : {});
499
+ }
500
+ shares() {
501
+ return this.request("GET", "/api/shares");
502
+ }
503
+ revokeShare(shareId) {
504
+ return this.request("DELETE", `/api/shares/${encodeURIComponent(shareId)}`);
505
+ }
495
506
  setAgentPermissions(id, agentId, permissions) {
496
507
  return this.request("POST", `/api/projects/${encodeURIComponent(id)}/agents/${encodeURIComponent(agentId)}/permissions`, { permissions });
497
508
  }
@@ -629,6 +640,9 @@ export async function desktopOwnsDaemon(cfg = readDaemonConfig()) {
629
640
  * stale daemons are the classic "failed to fetch / missing route" cause.
630
641
  */
631
642
  export async function ensureDaemon() {
643
+ // another loom command is swapping the daemon for a newer build: wait for it rather than
644
+ // reach the one going away (or start a second) — a script's next step lands mid-restart
645
+ await waitForRestart();
632
646
  let cfg = readDaemonConfig();
633
647
  if (cfg) {
634
648
  const probe = await healthProbe(cfg);
@@ -649,13 +663,75 @@ export async function ensureDaemon() {
649
663
  "using it as it is; restart when they finish: loom up --restart\n");
650
664
  return new DaemonClient(cfg);
651
665
  }
652
- await stopDaemon();
653
- const gone = Date.now() + 5000;
654
- while (Date.now() < gone && (await healthy(cfg))) {
655
- await new Promise((r) => setTimeout(r, 200));
666
+ // hold the restart lock until the new daemon answers; someone else holding it is restarting already
667
+ if (!takeRestartLock()) {
668
+ await waitForRestart();
669
+ const now = readDaemonConfig();
670
+ if (now && (await healthy(now)))
671
+ return new DaemonClient(now);
672
+ }
673
+ else {
674
+ try {
675
+ await stopDaemon();
676
+ const gone = Date.now() + 5000;
677
+ while (Date.now() < gone && (await healthy(cfg))) {
678
+ await new Promise((r) => setTimeout(r, 200));
679
+ }
680
+ return await startDaemon();
681
+ }
682
+ finally {
683
+ releaseRestartLock();
684
+ }
656
685
  }
657
686
  }
658
687
  }
688
+ return startDaemon();
689
+ }
690
+ const RESTART_LOCK_MS = 30_000;
691
+ const restartLockFile = () => path.join(loomHome(), "daemon.restarting");
692
+ /** Mark "this CLI is restarting the daemon"; false when another one already is (a fresh lock). */
693
+ function takeRestartLock() {
694
+ const file = restartLockFile();
695
+ try {
696
+ fs.writeFileSync(file, JSON.stringify({ pid: process.pid, at: Date.now() }), { flag: "wx" });
697
+ return true;
698
+ }
699
+ catch {
700
+ // a lock left by a CLI that died mid-restart is stale after RESTART_LOCK_MS: take it over
701
+ try {
702
+ if (Date.now() - fs.statSync(file).mtimeMs > RESTART_LOCK_MS) {
703
+ fs.writeFileSync(file, JSON.stringify({ pid: process.pid, at: Date.now() }));
704
+ return true;
705
+ }
706
+ }
707
+ catch { /* gone in between: someone finished */ }
708
+ return false;
709
+ }
710
+ }
711
+ function releaseRestartLock() {
712
+ try {
713
+ fs.rmSync(restartLockFile(), { force: true });
714
+ }
715
+ catch { /* already gone */ }
716
+ }
717
+ /** Wait while another loom command holds a fresh restart lock (at most RESTART_LOCK_MS). */
718
+ async function waitForRestart() {
719
+ const file = restartLockFile();
720
+ const until = Date.now() + RESTART_LOCK_MS;
721
+ while (Date.now() < until) {
722
+ try {
723
+ if (Date.now() - fs.statSync(file).mtimeMs > RESTART_LOCK_MS)
724
+ return;
725
+ }
726
+ catch {
727
+ return;
728
+ }
729
+ await new Promise((r) => setTimeout(r, 250));
730
+ }
731
+ }
732
+ /** Start a detached daemon and wait for it to answer. */
733
+ async function startDaemon() {
734
+ let cfg = null;
659
735
  const compiled = fileURLToPath(new URL("../cli/index.js", import.meta.url));
660
736
  const devSource = fileURLToPath(new URL("../cli/index.ts", import.meta.url));
661
737
  const { existsSync } = await import("node:fs");