pair-mode 0.3.0 → 0.4.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 (53) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +2 -8
  3. package/README.md +71 -12
  4. package/assets/duck-appalled-banner-sprite.png +0 -0
  5. package/assets/duck-appalled-banner-v2-sprite.png +0 -0
  6. package/assets/duck-appalled-banner-v2.gif +0 -0
  7. package/assets/duck-appalled-sprite.png +0 -0
  8. package/assets/duck-appalled.gif +0 -0
  9. package/assets/duck-before-main.png +0 -0
  10. package/assets/duck.png +0 -0
  11. package/assets/favicon.png +0 -0
  12. package/assets/syntax/clojure.yaml +36 -0
  13. package/assets/syntax/cmake.yaml +41 -0
  14. package/assets/syntax/crystal.yaml +71 -0
  15. package/assets/syntax/csharp.yaml +51 -0
  16. package/assets/syntax/dart.yaml +45 -0
  17. package/assets/syntax/elm.yaml +38 -0
  18. package/assets/syntax/erb.yaml +42 -0
  19. package/assets/syntax/erlang.yaml +45 -0
  20. package/assets/syntax/fsharp.yaml +48 -0
  21. package/assets/syntax/graphql.yaml +47 -0
  22. package/assets/syntax/groovy.yaml +111 -0
  23. package/assets/syntax/haml.yaml +16 -0
  24. package/assets/syntax/haskell.yaml +52 -0
  25. package/assets/syntax/ini.yaml +23 -0
  26. package/assets/syntax/java.yaml +36 -0
  27. package/assets/syntax/julia.yaml +56 -0
  28. package/assets/syntax/kotlin.yaml +65 -0
  29. package/assets/syntax/makefile.yaml +37 -0
  30. package/assets/syntax/nginx.yaml +22 -0
  31. package/assets/syntax/nim.yaml +27 -0
  32. package/assets/syntax/nix.yaml +32 -0
  33. package/assets/syntax/objc.yaml +60 -0
  34. package/assets/syntax/ocaml.yaml +43 -0
  35. package/assets/syntax/perl.yaml +58 -0
  36. package/assets/syntax/php.yaml +60 -0
  37. package/assets/syntax/r.yaml +30 -0
  38. package/assets/syntax/scala.yaml +32 -0
  39. package/assets/syntax/svelte.yaml +27 -0
  40. package/assets/syntax/swift.yaml +102 -0
  41. package/assets/syntax/vue.yaml +63 -0
  42. package/assets/syntax/xml.yaml +37 -0
  43. package/assets/syntax/zig.yaml +52 -0
  44. package/dist/claude-code.js +463 -150
  45. package/dist/cli.js +1534 -459
  46. package/dist/codex.js +811 -394
  47. package/dist/opencode.js +455 -143
  48. package/dist/pair-tui.js +177 -27
  49. package/dist/pi.js +510 -156
  50. package/package.json +16 -17
  51. package/skills/toggle/SKILL.md +24 -0
  52. package/commands/pair.md +0 -18
  53. package/skills/pair/SKILL.md +0 -20
package/dist/opencode.js CHANGED
@@ -7363,11 +7363,72 @@ var require_dist = __commonJS({
7363
7363
  // src/core/state/state.ts
7364
7364
  import { createHash } from "node:crypto";
7365
7365
  import { homedir } from "node:os";
7366
- import { join, dirname, basename } from "node:path";
7367
- import { existsSync, realpathSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
7366
+ import { join as join2, dirname, basename } from "node:path";
7367
+ import { existsSync, realpathSync, mkdirSync, writeFileSync, unlinkSync as unlinkSync2 } from "node:fs";
7368
+
7369
+ // src/helpers/isRecord.ts
7370
+ function isRecord(value) {
7371
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7372
+ }
7373
+
7374
+ // src/helpers/readFileOrEmpty.ts
7375
+ import { readFileSync } from "node:fs";
7376
+ function readFileOrEmpty(path) {
7377
+ try {
7378
+ return readFileSync(path, "utf-8");
7379
+ } catch {
7380
+ return "";
7381
+ }
7382
+ }
7383
+
7384
+ // src/helpers/removeQuietly.ts
7385
+ import { unlinkSync } from "node:fs";
7386
+ function removeQuietly(path) {
7387
+ try {
7388
+ unlinkSync(path);
7389
+ } catch {
7390
+ }
7391
+ }
7392
+
7393
+ // src/helpers/resolvesOnPath.ts
7394
+ import { spawnSync } from "node:child_process";
7395
+ var defaultResolvesOnPath = (command) => {
7396
+ const result = spawnSync("which", [command], { stdio: "ignore" });
7397
+ return result.status === 0;
7398
+ };
7399
+
7400
+ // src/helpers/resultFilePath.ts
7401
+ import { randomBytes } from "node:crypto";
7402
+ import { tmpdir } from "node:os";
7403
+ import { join } from "node:path";
7404
+ var NAME_BYTES = 6;
7405
+ function resultFilePath() {
7406
+ const name = `pair-result-${randomBytes(NAME_BYTES).toString("hex")}.json`;
7407
+ return join(tmpdir(), name);
7408
+ }
7409
+
7410
+ // src/helpers/spawn.ts
7411
+ import { spawnSync as spawnSync2 } from "node:child_process";
7412
+ var defaultSpawn = (command, args) => {
7413
+ const result = spawnSync2(command, args, { stdio: ["ignore", "ignore", "pipe"] });
7414
+ const stderr = result.stderr ? result.stderr.toString("utf-8") : result.error?.message ?? "";
7415
+ return { status: result.status, stderr };
7416
+ };
7417
+
7418
+ // src/helpers/splitLines.ts
7419
+ function splitLines(text) {
7420
+ const lines = text.split("\n");
7421
+ const last = lines.at(-1);
7422
+ if (last === "") {
7423
+ lines.pop();
7424
+ }
7425
+ return lines;
7426
+ }
7427
+
7428
+ // src/core/state/state.ts
7368
7429
  function stateDir() {
7369
- const base = process.env["XDG_STATE_HOME"] || join(homedir(), ".local", "state");
7370
- return join(base, "pair-mode");
7430
+ const base = process.env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state");
7431
+ return join2(base, "pair-mode");
7371
7432
  }
7372
7433
  function realpathLenient(path) {
7373
7434
  try {
@@ -7377,7 +7438,7 @@ function realpathLenient(path) {
7377
7438
  if (parent === path) {
7378
7439
  return path;
7379
7440
  }
7380
- return join(realpathLenient(parent), basename(path));
7441
+ return join2(realpathLenient(parent), basename(path));
7381
7442
  }
7382
7443
  }
7383
7444
  var DIGEST_LENGTH = 16;
@@ -7386,13 +7447,13 @@ function digestFor(directory) {
7386
7447
  return createHash("sha1").update(real).digest("hex").slice(0, DIGEST_LENGTH);
7387
7448
  }
7388
7449
  function flagPath(directory) {
7389
- return join(stateDir(), `${digestFor(directory)}.on`);
7450
+ return join2(stateDir(), `${digestFor(directory)}.on`);
7390
7451
  }
7391
7452
  function sessionsDir() {
7392
- return join(stateDir(), "sessions");
7453
+ return join2(stateDir(), "sessions");
7393
7454
  }
7394
7455
  function sessionSocketPath(directory) {
7395
- return join(sessionsDir(), `${digestFor(directory)}.sock`);
7456
+ return join2(sessionsDir(), `${digestFor(directory)}.sock`);
7396
7457
  }
7397
7458
  function findSessionSocket(filePath) {
7398
7459
  let current = dirname(realpathLenient(filePath));
@@ -7408,7 +7469,13 @@ function findSessionSocket(filePath) {
7408
7469
  current = parent;
7409
7470
  }
7410
7471
  }
7411
- function isEnabled(filePath) {
7472
+ function sessionFlagState(key) {
7473
+ if (existsSync(sessionKeyOptOutPath(key))) {
7474
+ return "off";
7475
+ }
7476
+ return existsSync(sessionKeyFlagPath(key)) ? "on" : "unset";
7477
+ }
7478
+ function directoryEnabled(filePath) {
7412
7479
  let current = dirname(realpathLenient(filePath));
7413
7480
  while (true) {
7414
7481
  if (existsSync(flagPath(current))) {
@@ -7421,10 +7488,34 @@ function isEnabled(filePath) {
7421
7488
  current = parent;
7422
7489
  }
7423
7490
  }
7424
-
7425
- // src/helpers/isRecord.ts
7426
- function isRecord(value) {
7427
- return typeof value === "object" && value !== null && !Array.isArray(value);
7491
+ function isEnabled(filePath, key) {
7492
+ if (key !== void 0) {
7493
+ const state = sessionFlagState(key);
7494
+ if (state !== "unset") {
7495
+ return state === "on";
7496
+ }
7497
+ }
7498
+ return directoryEnabled(filePath);
7499
+ }
7500
+ var SESSION_KEY_LENGTH = 8;
7501
+ function sessionKey(agentSessionId) {
7502
+ const digest = createHash("sha1").update(agentSessionId).digest("hex");
7503
+ return `s-${digest.slice(0, SESSION_KEY_LENGTH)}`;
7504
+ }
7505
+ function keyFor(agentSessionId) {
7506
+ return agentSessionId === void 0 || agentSessionId === "" ? void 0 : sessionKey(agentSessionId);
7507
+ }
7508
+ function sessionKeyPath(key, extension) {
7509
+ return join2(sessionsDir(), `${key}${extension}`);
7510
+ }
7511
+ function sessionKeySocketPath(key) {
7512
+ return sessionKeyPath(key, ".sock");
7513
+ }
7514
+ function sessionKeyFlagPath(key) {
7515
+ return sessionKeyPath(key, ".on");
7516
+ }
7517
+ function sessionKeyOptOutPath(key) {
7518
+ return sessionKeyPath(key, ".off");
7428
7519
  }
7429
7520
 
7430
7521
  // src/core/simulate/simulate.ts
@@ -7471,7 +7562,7 @@ function applyEdit(text, edit) {
7471
7562
  }
7472
7563
  return replaceFirst(text, old, next);
7473
7564
  }
7474
- function simulate(tool, input, readFile) {
7565
+ function simulate(tool, input, readFile, sessionId) {
7475
7566
  if (!isRecord(input)) {
7476
7567
  return null;
7477
7568
  }
@@ -7483,7 +7574,7 @@ function simulate(tool, input, readFile) {
7483
7574
  const content = input["content"];
7484
7575
  const after = typeof content === "string" ? content : "";
7485
7576
  const before = readFile(filePath);
7486
- return { tool, filePath, before, after };
7577
+ return { tool, filePath, before, after, sessionId };
7487
7578
  }
7488
7579
  if (tool === "Edit" || tool === "MultiEdit") {
7489
7580
  const editsRaw = input["edits"];
@@ -7498,7 +7589,7 @@ function simulate(tool, input, readFile) {
7498
7589
  if (after === null) {
7499
7590
  return null;
7500
7591
  }
7501
- return { tool, filePath, before, after };
7592
+ return { tool, filePath, before, after, sessionId };
7502
7593
  }
7503
7594
  return null;
7504
7595
  }
@@ -7929,16 +8020,6 @@ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, rmSync
7929
8020
  import { tmpdir as tmpdir2 } from "node:os";
7930
8021
  import { join as join8 } from "node:path";
7931
8022
 
7932
- // src/helpers/splitLines.ts
7933
- function splitLines(text) {
7934
- const lines = text.split("\n");
7935
- const last = lines.at(-1);
7936
- if (last === "") {
7937
- lines.pop();
7938
- }
7939
- return lines;
7940
- }
7941
-
7942
8023
  // src/core/render/render.ts
7943
8024
  var HEADER_LEAD = [
7944
8025
  "# PAIR MODE. Left pane is the current file. Right pane is the proposal.",
@@ -7977,56 +8058,299 @@ function renderInline(input) {
7977
8058
  // src/editors/micro.ts
7978
8059
  var import_yaml = __toESM(require_dist(), 1);
7979
8060
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
7980
- import { join as join3 } from "node:path";
8061
+ import { join as join4 } from "node:path";
7981
8062
 
7982
8063
  // src/editors/languages.ts
7983
- import { extname } from "node:path";
7984
- var LANGS = {
8064
+ import { openSync, readSync, closeSync } from "node:fs";
8065
+ import { basename as basename2, extname } from "node:path";
8066
+ var EXTENSIONS = {
7985
8067
  ".go": "go",
7986
8068
  ".rb": "ruby",
7987
8069
  ".rake": "ruby",
8070
+ ".gemspec": "ruby",
8071
+ ".ru": "ruby",
8072
+ ".erb": "erb",
8073
+ ".haml": "haml",
7988
8074
  ".ts": "typescript",
7989
- ".tsx": "typescript",
8075
+ ".mts": "typescript",
8076
+ ".cts": "typescript",
8077
+ ".tsx": "tsx",
7990
8078
  ".js": "javascript",
7991
- ".jsx": "javascript",
7992
8079
  ".mjs": "javascript",
7993
- ".py": "python3",
8080
+ ".cjs": "javascript",
8081
+ ".jsx": "jsx",
8082
+ ".vue": "vue",
8083
+ ".svelte": "svelte",
8084
+ ".astro": "astro",
8085
+ ".py": "python",
8086
+ ".pyi": "python",
7994
8087
  ".ex": "elixir",
7995
8088
  ".exs": "elixir",
8089
+ ".heex": "elixir",
8090
+ ".erl": "erlang",
8091
+ ".hrl": "erlang",
7996
8092
  ".rs": "rust",
7997
- ".sh": "sh",
7998
- ".bash": "sh",
7999
- ".fish": "fish",
8093
+ ".sh": "shellscript",
8094
+ ".bash": "shellscript",
8095
+ ".ksh": "shellscript",
8000
8096
  ".zsh": "zsh",
8097
+ ".fish": "fish",
8098
+ ".ps1": "powershell",
8099
+ ".psm1": "powershell",
8100
+ ".bat": "bat",
8101
+ ".cmd": "bat",
8001
8102
  ".sql": "sql",
8002
8103
  ".json": "json",
8104
+ ".jsonc": "jsonc",
8105
+ ".json5": "json5",
8003
8106
  ".tf": "terraform",
8107
+ ".tfvars": "terraform",
8108
+ ".hcl": "hcl",
8004
8109
  ".proto": "proto",
8005
- ".dockerfile": "dockerfile",
8110
+ ".dockerfile": "docker",
8006
8111
  ".toml": "toml",
8007
8112
  ".yaml": "yaml",
8008
8113
  ".yml": "yaml",
8114
+ ".ini": "ini",
8115
+ ".cfg": "ini",
8116
+ ".properties": "properties",
8117
+ ".env": "dotenv",
8009
8118
  ".md": "markdown",
8119
+ ".markdown": "markdown",
8120
+ ".mdx": "mdx",
8010
8121
  ".css": "css",
8122
+ ".scss": "scss",
8123
+ ".sass": "sass",
8124
+ ".less": "less",
8125
+ ".styl": "stylus",
8011
8126
  ".html": "html",
8012
- ".erb": "html",
8127
+ ".htm": "html",
8128
+ ".xml": "xml",
8129
+ ".xsl": "xml",
8130
+ ".svg": "xml",
8013
8131
  ".lua": "lua",
8014
8132
  ".c": "c",
8015
- ".h": "c"
8133
+ ".h": "c",
8134
+ ".cc": "cpp",
8135
+ ".cpp": "cpp",
8136
+ ".cxx": "cpp",
8137
+ ".hpp": "cpp",
8138
+ ".hh": "cpp",
8139
+ ".cs": "csharp",
8140
+ ".java": "java",
8141
+ ".kt": "kotlin",
8142
+ ".kts": "kotlin",
8143
+ ".swift": "swift",
8144
+ ".m": "objective-c",
8145
+ ".mm": "objective-c",
8146
+ ".php": "php",
8147
+ ".pl": "perl",
8148
+ ".pm": "perl",
8149
+ ".scala": "scala",
8150
+ ".sc": "scala",
8151
+ ".clj": "clojure",
8152
+ ".cljs": "clojure",
8153
+ ".cljc": "clojure",
8154
+ ".hs": "haskell",
8155
+ ".nix": "nix",
8156
+ ".r": "r",
8157
+ ".jl": "julia",
8158
+ ".zig": "zig",
8159
+ ".nim": "nim",
8160
+ ".dart": "dart",
8161
+ ".groovy": "groovy",
8162
+ ".gradle": "groovy",
8163
+ ".cr": "crystal",
8164
+ ".ml": "ocaml",
8165
+ ".mli": "ocaml",
8166
+ ".fs": "fsharp",
8167
+ ".fsx": "fsharp",
8168
+ ".elm": "elm",
8169
+ ".graphql": "graphql",
8170
+ ".gql": "graphql",
8171
+ ".prisma": "prisma",
8172
+ ".sol": "solidity",
8173
+ ".vim": "viml",
8174
+ ".diff": "diff",
8175
+ ".patch": "diff",
8176
+ ".tex": "latex",
8177
+ ".wgsl": "wgsl",
8178
+ ".glsl": "glsl",
8179
+ ".cue": "cue"
8180
+ };
8181
+ var FILENAMES = {
8182
+ gemfile: "ruby",
8183
+ rakefile: "ruby",
8184
+ capfile: "ruby",
8185
+ vagrantfile: "ruby",
8186
+ guardfile: "ruby",
8187
+ podfile: "ruby",
8188
+ fastfile: "ruby",
8189
+ appfile: "ruby",
8190
+ brewfile: "ruby",
8191
+ "config.ru": "ruby",
8192
+ dockerfile: "docker",
8193
+ containerfile: "docker",
8194
+ makefile: "make",
8195
+ gnumakefile: "make",
8196
+ "cmakelists.txt": "cmake",
8197
+ "cargo.lock": "toml",
8198
+ "gemfile.lock": "toml",
8199
+ ".babelrc": "json",
8200
+ ".eslintrc": "json",
8201
+ ".prettierrc": "json",
8202
+ ".env": "dotenv",
8203
+ ".gitconfig": "ini",
8204
+ ".editorconfig": "ini",
8205
+ "nginx.conf": "nginx",
8206
+ "pnpm-workspace.yaml": "yaml"
8207
+ };
8208
+ var SHEBANGS = [
8209
+ { pattern: /^#!.*\/(env\s+)?ruby(\s|$)/, id: "ruby" },
8210
+ { pattern: /^#!.*\/(env\s+)?python[\d.]*(\s|$)/, id: "python" },
8211
+ { pattern: /^#!.*\/(env\s+)?(node|bun|deno)(\s|$)/, id: "javascript" },
8212
+ { pattern: /^#!.*\/(env\s+)?(bash|sh|ksh|dash)(\s|$)/, id: "shellscript" },
8213
+ { pattern: /^#!.*\/(env\s+)?zsh(\s|$)/, id: "zsh" },
8214
+ { pattern: /^#!.*\/(env\s+)?fish(\s|$)/, id: "fish" },
8215
+ { pattern: /^#!.*\/(env\s+)?perl(\s|$)/, id: "perl" },
8216
+ { pattern: /^#!.*\/(env\s+)?php(\s|$)/, id: "php" },
8217
+ { pattern: /^#!.*\/(env\s+)?(elixir|iex)(\s|$)/, id: "elixir" },
8218
+ { pattern: /^#!.*\/(env\s+)?lua(\s|$)/, id: "lua" },
8219
+ { pattern: /^#!.*\/(env\s+)?Rscript(\s|$)/, id: "r" },
8220
+ { pattern: /^#!.*\/(env\s+)?pwsh(\s|$)/, id: "powershell" }
8221
+ ];
8222
+ var FIRST_LINE_BYTES = 256;
8223
+ function firstLine(sourcePath) {
8224
+ let handle = null;
8225
+ try {
8226
+ handle = openSync(sourcePath, "r");
8227
+ const buffer = Buffer.alloc(FIRST_LINE_BYTES);
8228
+ const read = readSync(handle, buffer, 0, FIRST_LINE_BYTES, 0);
8229
+ const text = buffer.toString("utf-8", 0, read);
8230
+ const newline = text.indexOf("\n");
8231
+ return newline === -1 ? text : text.slice(0, newline);
8232
+ } catch {
8233
+ return null;
8234
+ } finally {
8235
+ if (handle !== null) {
8236
+ try {
8237
+ closeSync(handle);
8238
+ } catch {
8239
+ }
8240
+ }
8241
+ }
8242
+ }
8243
+ function shebangLanguage(sourcePath) {
8244
+ const line = firstLine(sourcePath);
8245
+ if (line === null || !line.startsWith("#!")) {
8246
+ return null;
8247
+ }
8248
+ return SHEBANGS.find((rule) => rule.pattern.test(line))?.id ?? null;
8249
+ }
8250
+ function detectLanguage(sourcePath) {
8251
+ const name = basename2(sourcePath).toLowerCase();
8252
+ const ext = extname(name);
8253
+ if (ext !== "" && EXTENSIONS[ext] !== void 0) {
8254
+ return EXTENSIONS[ext];
8255
+ }
8256
+ if (FILENAMES[name] !== void 0) {
8257
+ return FILENAMES[name];
8258
+ }
8259
+ return shebangLanguage(sourcePath);
8260
+ }
8261
+ var MICRO_SYNTAX = {
8262
+ c: "c",
8263
+ clojure: "clojure",
8264
+ cmake: "cmake",
8265
+ crystal: "crystal",
8266
+ csharp: "csharp",
8267
+ css: "css",
8268
+ dart: "dart",
8269
+ docker: "dockerfile",
8270
+ elixir: "elixir",
8271
+ elm: "elm",
8272
+ erb: "erb",
8273
+ erlang: "erlang",
8274
+ fish: "fish",
8275
+ fsharp: "fsharp",
8276
+ go: "go",
8277
+ graphql: "graphql",
8278
+ groovy: "groovy",
8279
+ haml: "haml",
8280
+ haskell: "haskell",
8281
+ html: "html",
8282
+ ini: "ini",
8283
+ java: "java",
8284
+ javascript: "javascript",
8285
+ json: "json",
8286
+ julia: "julia",
8287
+ kotlin: "kotlin",
8288
+ lua: "lua",
8289
+ make: "makefile",
8290
+ markdown: "markdown",
8291
+ nginx: "nginx",
8292
+ nim: "nim",
8293
+ nix: "nix",
8294
+ "objective-c": "objc",
8295
+ ocaml: "ocaml",
8296
+ perl: "perl",
8297
+ php: "php",
8298
+ proto: "proto",
8299
+ python: "python3",
8300
+ r: "r",
8301
+ ruby: "ruby",
8302
+ rust: "rust",
8303
+ scala: "scala",
8304
+ shellscript: "sh",
8305
+ sql: "sql",
8306
+ svelte: "svelte",
8307
+ swift: "swift",
8308
+ terraform: "terraform",
8309
+ toml: "toml",
8310
+ typescript: "typescript",
8311
+ vue: "vue",
8312
+ xml: "xml",
8313
+ yaml: "yaml",
8314
+ zig: "zig",
8315
+ zsh: "zsh"
8016
8316
  };
8017
- function syntaxName(sourcePath) {
8018
- const ext = extname(sourcePath).toLowerCase();
8019
- return LANGS[ext] ?? null;
8317
+ var VIM_FILETYPES = {
8318
+ shellscript: "sh",
8319
+ docker: "dockerfile",
8320
+ csharp: "cs",
8321
+ "objective-c": "objc",
8322
+ bat: "dosbatch",
8323
+ viml: "vim",
8324
+ mdx: "markdown",
8325
+ jsx: "javascriptreact",
8326
+ tsx: "typescriptreact",
8327
+ dotenv: "sh",
8328
+ properties: "jproperties",
8329
+ stylus: "stylus"
8330
+ };
8331
+ function microSyntaxName(sourcePath) {
8332
+ const id = detectLanguage(sourcePath);
8333
+ if (id === null) {
8334
+ return null;
8335
+ }
8336
+ return MICRO_SYNTAX[id] ?? null;
8337
+ }
8338
+ function vimFiletype(sourcePath) {
8339
+ const id = detectLanguage(sourcePath);
8340
+ if (id === null) {
8341
+ return null;
8342
+ }
8343
+ return VIM_FILETYPES[id] ?? id;
8020
8344
  }
8021
8345
 
8022
8346
  // src/editors/syntax-cache.ts
8023
- import { existsSync as existsSync2, readFileSync } from "node:fs";
8024
- import { dirname as dirname2, join as join2 } from "node:path";
8347
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
8348
+ import { dirname as dirname2, join as join3 } from "node:path";
8025
8349
  import { fileURLToPath } from "node:url";
8026
8350
  function defaultAssetsDir() {
8027
8351
  let dir = dirname2(fileURLToPath(import.meta.url));
8028
8352
  while (true) {
8029
- const candidate = join2(dir, "assets", "syntax");
8353
+ const candidate = join3(dir, "assets", "syntax");
8030
8354
  if (existsSync2(candidate)) {
8031
8355
  return candidate;
8032
8356
  }
@@ -8041,20 +8365,13 @@ function syntaxSource(lang, assetsDir = defaultAssetsDir()) {
8041
8365
  if (assetsDir === null) {
8042
8366
  return null;
8043
8367
  }
8044
- const path = join2(assetsDir, `${lang}.yaml`);
8368
+ const path = join3(assetsDir, `${lang}.yaml`);
8045
8369
  if (!existsSync2(path)) {
8046
8370
  return null;
8047
8371
  }
8048
- return readFileSync(path, "utf-8");
8372
+ return readFileSync2(path, "utf-8");
8049
8373
  }
8050
8374
 
8051
- // src/helpers/resolvesOnPath.ts
8052
- import { spawnSync } from "node:child_process";
8053
- var defaultResolvesOnPath = (command) => {
8054
- const result = spawnSync("which", [command], { stdio: "ignore" });
8055
- return result.status === 0;
8056
- };
8057
-
8058
8375
  // src/editors/micro.ts
8059
8376
  var MICRO_BINDINGS = {
8060
8377
  F2: "QuitAll",
@@ -8085,7 +8402,7 @@ function bandRules() {
8085
8402
  ];
8086
8403
  }
8087
8404
  function writeColorScheme(configDir, theme) {
8088
- const dir = join3(configDir, "colorschemes");
8405
+ const dir = join4(configDir, "colorschemes");
8089
8406
  mkdirSync2(dir, { recursive: true });
8090
8407
  const text = `include "monokai"
8091
8408
 
@@ -8093,7 +8410,7 @@ color-link pairadd "#d7ffd7,${theme.add}"
8093
8410
  color-link pairdel "#ffd7d7,${theme.del}"
8094
8411
  color-link pairskip "#6a6a6a,${theme.fold}"
8095
8412
  `;
8096
- writeFileSync2(join3(dir, "pair.micro"), text, "utf-8");
8413
+ writeFileSync2(join4(dir, "pair.micro"), text, "utf-8");
8097
8414
  }
8098
8415
  function syntaxText(lang, source) {
8099
8416
  const parsed = (0, import_yaml.parse)(source);
@@ -8109,7 +8426,7 @@ function syntaxText(lang, source) {
8109
8426
  return (0, import_yaml.stringify)(file, { lineWidth: 0 });
8110
8427
  }
8111
8428
  function writeSyntax(configDir, sourcePath) {
8112
- const lang = syntaxName(sourcePath);
8429
+ const lang = microSyntaxName(sourcePath);
8113
8430
  if (lang === null) {
8114
8431
  return;
8115
8432
  }
@@ -8121,9 +8438,9 @@ function writeSyntax(configDir, sourcePath) {
8121
8438
  if (text === null) {
8122
8439
  return;
8123
8440
  }
8124
- const dir = join3(configDir, "syntax");
8441
+ const dir = join4(configDir, "syntax");
8125
8442
  mkdirSync2(dir, { recursive: true });
8126
- writeFileSync2(join3(dir, `pair-${lang}.yaml`), text, "utf-8");
8443
+ writeFileSync2(join4(dir, `pair-${lang}.yaml`), text, "utf-8");
8127
8444
  }
8128
8445
  function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8129
8446
  return {
@@ -8136,7 +8453,7 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8136
8453
  return ["# F3 moves between panes. Ctrl+W or F2 sends and closes."];
8137
8454
  },
8138
8455
  bufferSuffix(sourcePath) {
8139
- const lang = syntaxName(sourcePath);
8456
+ const lang = microSyntaxName(sourcePath);
8140
8457
  if (lang === null) {
8141
8458
  return ".diff";
8142
8459
  }
@@ -8145,12 +8462,12 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8145
8462
  prepare(context) {
8146
8463
  mkdirSync2(context.configDir, { recursive: true });
8147
8464
  writeFileSync2(
8148
- join3(context.configDir, "bindings.json"),
8465
+ join4(context.configDir, "bindings.json"),
8149
8466
  JSON.stringify(MICRO_BINDINGS, null, 2),
8150
8467
  "utf-8"
8151
8468
  );
8152
8469
  writeFileSync2(
8153
- join3(context.configDir, "settings.json"),
8470
+ join4(context.configDir, "settings.json"),
8154
8471
  JSON.stringify(MICRO_SETTINGS, null, 2),
8155
8472
  "utf-8"
8156
8473
  );
@@ -8174,16 +8491,6 @@ function isHexColor(value) {
8174
8491
  }
8175
8492
 
8176
8493
  // src/editors/vim.ts
8177
- var VIM_FILETYPE_OVERRIDES = {
8178
- python3: "python"
8179
- };
8180
- function vimFiletype(sourcePath) {
8181
- const lang = syntaxName(sourcePath);
8182
- if (lang === null) {
8183
- return null;
8184
- }
8185
- return VIM_FILETYPE_OVERRIDES[lang] ?? lang;
8186
- }
8187
8494
  function safeThemeColor(value) {
8188
8495
  if (!isHexColor(value)) {
8189
8496
  throw new Error(`invalid theme colour for vim highlight: ${value}`);
@@ -8238,7 +8545,7 @@ function vimEditor(name, resolvesOnPath = defaultResolvesOnPath) {
8238
8545
 
8239
8546
  // src/editors/nano.ts
8240
8547
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
8241
- import { join as join4 } from "node:path";
8548
+ import { join as join5 } from "node:path";
8242
8549
  function safeThemeColor2(value) {
8243
8550
  if (!isHexColor(value)) {
8244
8551
  throw new Error(`invalid theme colour for nano rcfile: ${value}`);
@@ -8250,7 +8557,7 @@ function writeNanorc(configDir, theme) {
8250
8557
  color ,${safeThemeColor2(theme.del)} "^\u258C\u258C-"
8251
8558
  color ,${safeThemeColor2(theme.fold)} "^\u22EF"
8252
8559
  `;
8253
- const path = join4(configDir, "pair.nanorc");
8560
+ const path = join5(configDir, "pair.nanorc");
8254
8561
  writeFileSync3(path, text, "utf-8");
8255
8562
  return path;
8256
8563
  }
@@ -8280,13 +8587,13 @@ function createNanoEditor(resolvesOnPath = defaultResolvesOnPath) {
8280
8587
 
8281
8588
  // src/editors/pair.ts
8282
8589
  import { existsSync as existsSync4 } from "node:fs";
8283
- import { dirname as dirname4, extname as extname3, join as join6 } from "node:path";
8590
+ import { dirname as dirname4, extname as extname3, join as join7 } from "node:path";
8284
8591
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8285
8592
 
8286
8593
  // src/core/config/config.ts
8287
8594
  import { homedir as homedir2 } from "node:os";
8288
- import { join as join5, dirname as dirname3 } from "node:path";
8289
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "node:fs";
8595
+ import { join as join6, dirname as dirname3 } from "node:path";
8596
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "node:fs";
8290
8597
  var EDITOR_NAMES = ["auto", "pair", "micro", "nvim", "vim", "nano"];
8291
8598
  var MULTIPLEXER_NAMES = ["auto", "zellij", "tmux", "none"];
8292
8599
  var TRANSPORT_NAMES = ["pane", "session"];
@@ -8312,8 +8619,8 @@ var DEFAULT_CONFIG = {
8312
8619
  syntax: true
8313
8620
  };
8314
8621
  function configPath(homeDir) {
8315
- const base = process.env["XDG_CONFIG_HOME"] || join5(homeDir ?? homedir2(), ".config");
8316
- return join5(base, "pair-mode", "config.json");
8622
+ const base = process.env["XDG_CONFIG_HOME"] || join6(homeDir ?? homedir2(), ".config");
8623
+ return join6(base, "pair-mode", "config.json");
8317
8624
  }
8318
8625
  function isEditorName(value) {
8319
8626
  return typeof value === "string" && EDITOR_NAMES.includes(value);
@@ -8585,7 +8892,7 @@ function loadConfig(path) {
8585
8892
  }
8586
8893
  let text;
8587
8894
  try {
8588
- text = readFileSync2(target, "utf-8");
8895
+ text = readFileSync3(target, "utf-8");
8589
8896
  } catch (error) {
8590
8897
  const message = error instanceof Error ? error.message : String(error);
8591
8898
  return {
@@ -8620,7 +8927,7 @@ function paintLayout(layout) {
8620
8927
  // src/editors/pair.ts
8621
8928
  var defaultResolveTuiEntry = () => {
8622
8929
  const here = dirname4(fileURLToPath2(import.meta.url));
8623
- return join6(here, "pair-tui.js");
8930
+ return join7(here, "pair-tui.js");
8624
8931
  };
8625
8932
  var defaultCheckBundleExists = existsSync4;
8626
8933
  function createPairEditor(resolveTuiEntry = defaultResolveTuiEntry, checkBundleExists = defaultCheckBundleExists) {
@@ -8719,14 +9026,6 @@ function resolve(preference, resolvesOnPath = defaultResolvesOnPath, checkPairBu
8719
9026
  return available ?? vimEditor("vim", resolvesOnPath);
8720
9027
  }
8721
9028
 
8722
- // src/helpers/spawn.ts
8723
- import { spawnSync as spawnSync2 } from "node:child_process";
8724
- var defaultSpawn = (command, args) => {
8725
- const result = spawnSync2(command, args, { stdio: ["ignore", "ignore", "pipe"] });
8726
- const stderr = result.stderr ? result.stderr.toString("utf-8") : result.error?.message ?? "";
8727
- return { status: result.status, stderr };
8728
- };
8729
-
8730
9029
  // src/multiplexers/zellij.ts
8731
9030
  function createZellijMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultResolvesOnPath) {
8732
9031
  return {
@@ -8756,7 +9055,7 @@ function createZellijMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultR
8756
9055
  }
8757
9056
 
8758
9057
  // src/multiplexers/tmux.ts
8759
- import { randomBytes } from "node:crypto";
9058
+ import { randomBytes as randomBytes2 } from "node:crypto";
8760
9059
  var CHANNEL_BYTES = 8;
8761
9060
  var SAFE_UNQUOTED = /^[A-Za-z0-9_\-./]+$/;
8762
9061
  function shellQuote(value) {
@@ -8772,7 +9071,7 @@ function createTmuxMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultRes
8772
9071
  return Boolean(process.env["TMUX"]) && resolvesOnPath("tmux");
8773
9072
  },
8774
9073
  run(argv, size) {
8775
- const channel = `pair-${randomBytes(CHANNEL_BYTES).toString("hex")}`;
9074
+ const channel = `pair-${randomBytes2(CHANNEL_BYTES).toString("hex")}`;
8776
9075
  const inner = argv.map(shellQuote).join(" ");
8777
9076
  const script = `stty -ixon 2>/dev/null; ${inner}; tmux wait-for -S ${channel}`;
8778
9077
  const popup = spawn("tmux", [
@@ -8794,9 +9093,9 @@ function createTmuxMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultRes
8794
9093
  }
8795
9094
 
8796
9095
  // src/multiplexers/tty.ts
8797
- import { openSync, closeSync } from "node:fs";
9096
+ import { openSync as openSync2, closeSync as closeSync2 } from "node:fs";
8798
9097
  import { spawnSync as spawnSync3 } from "node:child_process";
8799
- var defaultOpen = () => openSync("/dev/tty", "r+");
9098
+ var defaultOpen = () => openSync2("/dev/tty", "r+");
8800
9099
  var defaultRunner = (command, args, fd) => {
8801
9100
  const result = spawnSync3(command, args, { stdio: [fd, fd, fd] });
8802
9101
  const detail = result.status === 0 ? "" : String(result.error?.message ?? result.stderr ?? "");
@@ -8804,7 +9103,7 @@ var defaultRunner = (command, args, fd) => {
8804
9103
  };
8805
9104
  function closeQuietly(fd) {
8806
9105
  try {
8807
- closeSync(fd);
9106
+ closeSync2(fd);
8808
9107
  } catch {
8809
9108
  }
8810
9109
  }
@@ -8861,44 +9160,16 @@ function detect(preference, adapters = {}) {
8861
9160
  return tty;
8862
9161
  }
8863
9162
 
8864
- // src/helpers/readFileOrEmpty.ts
8865
- import { readFileSync as readFileSync3 } from "node:fs";
8866
- function readFileOrEmpty(path) {
8867
- try {
8868
- return readFileSync3(path, "utf-8");
8869
- } catch {
8870
- return "";
8871
- }
8872
- }
8873
-
8874
- // src/helpers/removeQuietly.ts
8875
- import { unlinkSync as unlinkSync2 } from "node:fs";
8876
- function removeQuietly(path) {
8877
- try {
8878
- unlinkSync2(path);
8879
- } catch {
8880
- }
8881
- }
8882
-
8883
- // src/helpers/resultFilePath.ts
8884
- import { randomBytes as randomBytes2 } from "node:crypto";
8885
- import { tmpdir } from "node:os";
8886
- import { join as join7 } from "node:path";
8887
- var NAME_BYTES = 6;
8888
- function resultFilePath() {
8889
- const name = `pair-result-${randomBytes2(NAME_BYTES).toString("hex")}.json`;
8890
- return join7(tmpdir(), name);
8891
- }
8892
-
8893
9163
  // src/transports/pane/pane.ts
8894
9164
  var NAME_BYTES2 = 6;
9165
+ var OWNER_ONLY_FILE = 384;
8895
9166
  function splitCommand(value) {
8896
9167
  return value.trim().split(/\s+/).filter((part) => part !== "");
8897
9168
  }
8898
9169
  function tempFile(prefix, suffix, content) {
8899
9170
  const name = `${prefix}${randomBytes3(NAME_BYTES2).toString("hex")}${suffix}`;
8900
9171
  const path = join8(tmpdir2(), name);
8901
- writeFileSync5(path, content, "utf-8");
9172
+ writeFileSync5(path, content, { encoding: "utf-8", mode: OWNER_ONLY_FILE, flag: "wx" });
8902
9173
  return path;
8903
9174
  }
8904
9175
  function removeTreeQuietly(path) {
@@ -8988,6 +9259,7 @@ function createPaneTransport(deps = {}) {
8988
9259
 
8989
9260
  // src/transports/session/client.ts
8990
9261
  import { createConnection } from "node:net";
9262
+ import { existsSync as existsSync5 } from "node:fs";
8991
9263
 
8992
9264
  // src/transports/session/wire.ts
8993
9265
  var CLIENT_KINDS = ["tui", "web"];
@@ -9002,11 +9274,11 @@ function createLineReader() {
9002
9274
  return parts.filter((line) => line.trim() !== "");
9003
9275
  };
9004
9276
  }
9005
- function isString(value) {
9277
+ function isString2(value) {
9006
9278
  return typeof value === "string";
9007
9279
  }
9008
9280
  function isClientKind(value) {
9009
- return isString(value) && CLIENT_KINDS.includes(value);
9281
+ return isString2(value) && CLIENT_KINDS.includes(value);
9010
9282
  }
9011
9283
  function isQuestion(value) {
9012
9284
  if (!isRecord(value)) {
@@ -9014,16 +9286,16 @@ function isQuestion(value) {
9014
9286
  }
9015
9287
  const line = value["line"];
9016
9288
  const lineOk = line === null || typeof line === "number";
9017
- return lineOk && isString(value["code"]) && isString(value["text"]);
9289
+ return lineOk && isString2(value["code"]) && isString2(value["text"]);
9018
9290
  }
9019
9291
  function isQuestionList(value) {
9020
9292
  return Array.isArray(value) && value.every(isQuestion);
9021
9293
  }
9022
9294
  function toSubmit(raw) {
9023
- if (!isString(raw["tool"]) || !isString(raw["path"])) {
9295
+ if (!isString2(raw["tool"]) || !isString2(raw["path"])) {
9024
9296
  return null;
9025
9297
  }
9026
- if (!isString(raw["before"]) || !isString(raw["after"])) {
9298
+ if (!isString2(raw["before"]) || !isString2(raw["after"])) {
9027
9299
  return null;
9028
9300
  }
9029
9301
  return {
@@ -9039,7 +9311,7 @@ function toAttach(raw) {
9039
9311
  }
9040
9312
  function toReview(raw) {
9041
9313
  const submit = toSubmit({ ...raw, type: "submit" });
9042
- if (submit === null || !isString(raw["id"])) {
9314
+ if (submit === null || !isString2(raw["id"])) {
9043
9315
  return null;
9044
9316
  }
9045
9317
  return {
@@ -9052,13 +9324,31 @@ function toReview(raw) {
9052
9324
  };
9053
9325
  }
9054
9326
  function toVerdict(raw) {
9055
- if (!isString(raw["id"]) || !isQuestionList(raw["questions"])) {
9327
+ if (!isString2(raw["id"]) || !isQuestionList(raw["questions"])) {
9056
9328
  return null;
9057
9329
  }
9058
9330
  return { type: "verdict", id: raw["id"], questions: raw["questions"] };
9059
9331
  }
9060
9332
  function toCancel(raw) {
9061
- return isString(raw["id"]) ? { type: "cancel", id: raw["id"] } : null;
9333
+ return isString2(raw["id"]) ? { type: "cancel", id: raw["id"] } : null;
9334
+ }
9335
+ function isNumber(value) {
9336
+ return typeof value === "number" && Number.isFinite(value);
9337
+ }
9338
+ function toState(raw) {
9339
+ const lastAttachAt = raw["lastAttachAt"];
9340
+ if (lastAttachAt !== null && !isString2(lastAttachAt)) {
9341
+ return null;
9342
+ }
9343
+ if (!isNumber(raw["clientCount"]) || !isNumber(raw["waitingDepth"])) {
9344
+ return null;
9345
+ }
9346
+ return {
9347
+ type: "state",
9348
+ clientCount: raw["clientCount"],
9349
+ waitingDepth: raw["waitingDepth"],
9350
+ lastAttachAt
9351
+ };
9062
9352
  }
9063
9353
  function decodeLine(line) {
9064
9354
  let parsed;
@@ -9086,11 +9376,18 @@ function decodeLine(line) {
9086
9376
  if (type === "cancel") {
9087
9377
  return toCancel(parsed);
9088
9378
  }
9379
+ if (type === "status") {
9380
+ return { type: "status" };
9381
+ }
9382
+ if (type === "state") {
9383
+ return toState(parsed);
9384
+ }
9089
9385
  return null;
9090
9386
  }
9091
9387
 
9092
9388
  // src/transports/session/client.ts
9093
9389
  var MS_PER_SECOND = 1e3;
9390
+ var NO_WATCHER = "no pair-mode watcher attached";
9094
9391
  function failOpen(detail) {
9095
9392
  return { reviewed: false, detail };
9096
9393
  }
@@ -9123,7 +9420,7 @@ function requestReview(request, options) {
9123
9420
  let lastError = "";
9124
9421
  socket.on("error", (error) => {
9125
9422
  if (isConnectionRefused(error)) {
9126
- settle(failOpen("no pair-mode watcher attached"));
9423
+ settle(failOpen(NO_WATCHER));
9127
9424
  return;
9128
9425
  }
9129
9426
  lastError = error instanceof Error ? error.message : String(error);
@@ -9153,15 +9450,27 @@ function requestReview(request, options) {
9153
9450
  });
9154
9451
  });
9155
9452
  }
9156
- function reviewInSession(request, config, socketPath) {
9157
- const path = socketPath ?? findSessionSocket(request.filePath);
9158
- if (path === null) {
9159
- return Promise.resolve(failOpen("no pair-mode watcher attached"));
9453
+ async function reviewInSession(request, config, socketPath) {
9454
+ const timeoutMs = config.session.timeout * MS_PER_SECOND;
9455
+ if (socketPath !== void 0) {
9456
+ return await requestReview(request, { socketPath, timeoutMs });
9160
9457
  }
9161
- return requestReview(request, {
9162
- socketPath: path,
9163
- timeoutMs: config.session.timeout * MS_PER_SECOND
9164
- });
9458
+ const key = keyFor(request.sessionId);
9459
+ const sessionPath = key === void 0 ? null : sessionKeySocketPath(key);
9460
+ if (sessionPath !== null && existsSync5(sessionPath)) {
9461
+ const outcome = await requestReview(request, { socketPath: sessionPath, timeoutMs });
9462
+ if (outcome.reviewed) {
9463
+ return outcome;
9464
+ }
9465
+ if (outcome.detail !== NO_WATCHER) {
9466
+ return outcome;
9467
+ }
9468
+ }
9469
+ const directoryPath = findSessionSocket(request.filePath);
9470
+ if (directoryPath === null) {
9471
+ return failOpen(NO_WATCHER);
9472
+ }
9473
+ return await requestReview(request, { socketPath: directoryPath, timeoutMs });
9165
9474
  }
9166
9475
  function createSessionTransport(socketPath) {
9167
9476
  return {
@@ -9182,7 +9491,8 @@ async function runPair(request, config, deps = {}) {
9182
9491
  if (request.before === request.after) {
9183
9492
  return { decision: "allow", reviewed: false };
9184
9493
  }
9185
- if (!isEnabled(request.filePath)) {
9494
+ const key = keyFor(request.sessionId);
9495
+ if (!isEnabled(request.filePath, key)) {
9186
9496
  return { decision: "allow", reviewed: false };
9187
9497
  }
9188
9498
  const transport = deps.transport ?? resolveTransport(config, deps);
@@ -9236,10 +9546,12 @@ async function evaluate(input, output, runPair2) {
9236
9546
  if (call === null) {
9237
9547
  return null;
9238
9548
  }
9239
- if (!isEnabled(call.filePath)) {
9549
+ const sessionId = input.sessionID === "" ? void 0 : input.sessionID;
9550
+ const key = keyFor(sessionId);
9551
+ if (!isEnabled(call.filePath, key)) {
9240
9552
  return null;
9241
9553
  }
9242
- const request = simulate(call.tool, call.input, readFileOrEmpty);
9554
+ const request = simulate(call.tool, call.input, readFileOrEmpty, sessionId);
9243
9555
  if (request === null) {
9244
9556
  return null;
9245
9557
  }