cortad 0.3.1-rc.9 → 0.3.2

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 (57) hide show
  1. package/README.md +8 -5
  2. package/lib/app-port.mjs +9 -0
  3. package/lib/auto-reach.mjs +88 -0
  4. package/lib/backing.mjs +230 -0
  5. package/lib/calls.mjs +23 -0
  6. package/lib/cli.mjs +115 -30
  7. package/lib/contact-text.mjs +167 -0
  8. package/lib/data.mjs +16 -22
  9. package/lib/dissection-text.mjs +232 -0
  10. package/lib/document.mjs +75 -0
  11. package/lib/env-secrets.mjs +48 -0
  12. package/lib/failed-text.mjs +36 -0
  13. package/lib/fresh.mjs +33 -12
  14. package/lib/health.mjs +61 -0
  15. package/lib/held.mjs +67 -0
  16. package/lib/home.mjs +55 -17
  17. package/lib/hosted.mjs +201 -0
  18. package/lib/identities.mjs +1 -1
  19. package/lib/keeping.mjs +162 -0
  20. package/lib/ledger.mjs +20 -0
  21. package/lib/lock.mjs +6 -1
  22. package/lib/mcp.mjs +52 -18
  23. package/lib/mint.mjs +179 -38
  24. package/lib/mongo.mjs +74 -0
  25. package/lib/numbers-text.mjs +68 -0
  26. package/lib/pg.mjs +118 -0
  27. package/lib/proc.mjs +100 -0
  28. package/lib/proof.mjs +1014 -0
  29. package/lib/proxy.mjs +184 -0
  30. package/lib/pyhook/cortad_sites.py +218 -0
  31. package/lib/pyhook/cortad_tied.py +291 -0
  32. package/lib/pyhook/sitecustomize.py +1175 -166
  33. package/lib/qdrant.mjs +47 -0
  34. package/lib/reach.mjs +223 -0
  35. package/lib/read-text.mjs +78 -24
  36. package/lib/redis.mjs +130 -0
  37. package/lib/register.mjs +36 -27
  38. package/lib/replay.mjs +464 -34
  39. package/lib/runner.mjs +85 -0
  40. package/lib/service.mjs +25 -0
  41. package/lib/spec.mjs +7 -4
  42. package/lib/start.mjs +33 -2
  43. package/lib/stick.mjs +65 -25
  44. package/lib/stores.mjs +347 -0
  45. package/lib/switches.mjs +36 -0
  46. package/lib/tether.mjs +9 -0
  47. package/lib/text.mjs +314 -179
  48. package/lib/tied.cjs +217 -0
  49. package/lib/trace.cjs +386 -514
  50. package/lib/verbs.mjs +155 -38
  51. package/lib/wire.cjs +882 -0
  52. package/lib/words.mjs +2 -0
  53. package/lib/writes.mjs +2 -2
  54. package/local.mjs +661 -297
  55. package/package.json +31 -1
  56. package/skill/SKILL.md +49 -42
  57. package/skill/references/results.md +279 -102
package/lib/register.mjs CHANGED
@@ -31,52 +31,54 @@ export function detectClients({ home = homedir(), env = process.env } = {}) {
31
31
  };
32
32
  }
33
33
 
34
- // Returns the names of the clients that now know Cortad, for the one line the command prints.
34
+ // The clients that now know Cortad, and the files under the home folder this call wrote, relative to
35
+ // it: a connect says what it changed only when it changed something.
35
36
  export async function registerAll({ home = homedir(), env = process.env, run = exec, skillSrc = SKILL_SRC, spec = cliSpec({ env }) } = {}) {
36
37
  const found = detectClients({ home, env });
37
38
  const server = serverFor(spec);
38
- const skill = (dir) => installSkill(dir, { src: skillSrc, spec });
39
- const added = [];
39
+ const clients = [];
40
+ const changed = [];
41
+ const wrote = (rel, did) => { if (did) changed.push(rel); };
42
+ const skill = (rel) => wrote(rel, installSkill(join(home, rel), { src: skillSrc, spec }));
40
43
  if (found.claude) {
41
- await registerClaude({ home, env, run, server });
42
- skill(join(home, ".claude", "skills", "cortad"));
43
- added.push("Claude Code");
44
+ wrote(".claude.json", await registerClaude({ home, env, run, server }));
45
+ skill(join(".claude", "skills", "cortad"));
46
+ clients.push("Claude Code");
44
47
  }
45
48
  if (found.codex) {
46
- await registerCodex({ home, env, run, server });
47
- added.push("Codex");
49
+ wrote(join(".codex", "config.toml"), await registerCodex({ home, env, run, server }));
50
+ clients.push("Codex");
48
51
  }
49
52
  if (found.cursor) {
50
- registerJson(join(home, ".cursor", "mcp.json"), { type: "stdio", ...server });
51
- added.push("Cursor");
53
+ wrote(join(".cursor", "mcp.json"), registerJson(join(home, ".cursor", "mcp.json"), { type: "stdio", ...server }));
54
+ clients.push("Cursor");
52
55
  }
53
56
  if (found.copilot) {
54
57
  // Copilot CLI keeps its servers in its own home, in its own shape: a "local" server with every tool.
55
- registerJson(join(home, ".copilot", "mcp-config.json"), { type: "local", ...server, tools: ["*"] });
56
- skill(join(home, ".copilot", "skills", "cortad"));
57
- added.push("Copilot");
58
+ wrote(join(".copilot", "mcp-config.json"), registerJson(join(home, ".copilot", "mcp-config.json"), { type: "local", ...server, tools: ["*"] }));
59
+ skill(join(".copilot", "skills", "cortad"));
60
+ clients.push("Copilot");
58
61
  }
59
62
  // The one skills folder Codex, Cursor and Copilot all read.
60
- if (found.codex || found.cursor || found.copilot) skill(join(home, ".agents", "skills", "cortad"));
61
- return added;
63
+ if (found.codex || found.cursor || found.copilot) skill(join(".agents", "skills", "cortad"));
64
+ return { clients, changed };
62
65
  }
63
66
 
64
67
  // The client's own command when it is here, so its config is written the way it writes it; the file
65
- // itself only when the folder exists without the binary on this PATH, or the command failed.
68
+ // itself only when the folder exists without the binary on this PATH, or the command failed. Each
69
+ // register step says whether it wrote.
66
70
  async function registerClaude({ home, env, run, server }) {
67
71
  const file = join(home, ".claude.json");
68
72
  if (onPath("claude", env)) {
69
73
  const add = () => run("claude", ["mcp", "add", "--scope", "user", "cortad", "--", server.command, ...server.args], { env });
70
- try { await add(); return; } catch (err) {
74
+ try { await add(); return true; } catch (err) {
71
75
  if (/already exists/i.test(String(err?.stderr ?? err?.message))) {
72
- if (same(readJson(file)?.mcpServers?.cortad, server)) return;
73
- try { await run("claude", ["mcp", "remove", "--scope", "user", "cortad"], { env }); await add(); return; } catch { /* the file below */ }
76
+ if (same(readJson(file)?.mcpServers?.cortad, server)) return false;
77
+ try { await run("claude", ["mcp", "remove", "--scope", "user", "cortad"], { env }); await add(); return true; } catch { /* the file below */ }
74
78
  }
75
79
  }
76
80
  }
77
- const config = readJson(file) ?? {};
78
- config.mcpServers = { ...(config.mcpServers ?? {}), cortad: { type: "stdio", ...server } };
79
- writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
81
+ return registerJson(file, { type: "stdio", ...server });
80
82
  }
81
83
 
82
84
  const CODEX_BLOCK = /^\[mcp_servers\.cortad\]\n(?:(?!\[).*(?:\n|$))*/m;
@@ -87,12 +89,12 @@ async function registerCodex({ home, env, run, server }) {
87
89
  const block = text.match(CODEX_BLOCK)?.[0] ?? "";
88
90
  const args = block.match(/^args\s*=\s*(\[.*\])\s*$/m)?.[1];
89
91
  const current = (() => { try { return args ? { command: block.match(/^command\s*=\s*"(.*)"\s*$/m)?.[1], args: JSON.parse(args) } : null; } catch { return null; } })();
90
- if (same(current, server)) return;
92
+ if (same(current, server)) return false;
91
93
  if (onPath("codex", env)) {
92
94
  try {
93
95
  if (block) await run("codex", ["mcp", "remove", "cortad"], { env });
94
96
  await run("codex", ["mcp", "add", "cortad", "--", server.command, ...server.args], { env });
95
- return;
97
+ return true;
96
98
  } catch { /* the file below */ }
97
99
  }
98
100
  const entry = `[mcp_servers.cortad]\ncommand = "${server.command}"\nargs = ${JSON.stringify(server.args)}\n`;
@@ -100,23 +102,30 @@ async function registerCodex({ home, env, run, server }) {
100
102
  // The blank lines after the old block stay, so the rest of the file keeps its layout.
101
103
  const replaced = () => text.replace(CODEX_BLOCK, (old) => `${entry}${(old.match(/\n+$/)?.[0] ?? "\n").slice(1)}`);
102
104
  writeFileSync(file, block ? replaced() : `${text}${text && !text.endsWith("\n") ? "\n" : ""}\n${entry}`);
105
+ return true;
103
106
  }
104
107
 
105
108
  function registerJson(file, entry) {
106
109
  const config = readJson(file) ?? {};
107
- if (same(config.mcpServers?.cortad, entry)) return;
110
+ if (same(config.mcpServers?.cortad, entry)) return false;
108
111
  config.mcpServers = { ...(config.mcpServers ?? {}), cortad: entry };
109
112
  mkdirSync(dirname(file), { recursive: true });
110
113
  writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
114
+ return true;
111
115
  }
112
116
 
113
117
  // SKILL.md and its references, copied whole so a newer package refreshes the words, with the
114
- // shell examples naming the spec this machine runs.
118
+ // shell examples naming the spec this machine runs. Says whether a file changed.
115
119
  export function installSkill(dir, { src = SKILL_SRC, spec = cliSpec() } = {}) {
116
120
  mkdirSync(join(dir, "references"), { recursive: true });
121
+ let wrote = false;
117
122
  for (const rel of ["SKILL.md", join("references", "results.md")]) {
118
- writeFileSync(join(dir, rel), readFileSync(join(src, rel), "utf8").replaceAll(PLACEHOLDER, spec));
123
+ const text = readFileSync(join(src, rel), "utf8").replaceAll(PLACEHOLDER, spec);
124
+ if (existsSync(join(dir, rel)) && readFileSync(join(dir, rel), "utf8") === text) continue;
125
+ writeFileSync(join(dir, rel), text);
126
+ wrote = true;
119
127
  }
128
+ return wrote;
120
129
  }
121
130
 
122
131
  function readJson(file) {