pair-mode 0.2.1 → 0.3.3

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 (45) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.codex-plugin/plugin.json +29 -0
  3. package/README.md +79 -0
  4. package/assets/syntax/clojure.yaml +36 -0
  5. package/assets/syntax/cmake.yaml +41 -0
  6. package/assets/syntax/crystal.yaml +71 -0
  7. package/assets/syntax/csharp.yaml +51 -0
  8. package/assets/syntax/dart.yaml +45 -0
  9. package/assets/syntax/elm.yaml +38 -0
  10. package/assets/syntax/erb.yaml +42 -0
  11. package/assets/syntax/erlang.yaml +45 -0
  12. package/assets/syntax/fsharp.yaml +48 -0
  13. package/assets/syntax/graphql.yaml +47 -0
  14. package/assets/syntax/groovy.yaml +111 -0
  15. package/assets/syntax/haml.yaml +16 -0
  16. package/assets/syntax/haskell.yaml +52 -0
  17. package/assets/syntax/ini.yaml +23 -0
  18. package/assets/syntax/java.yaml +36 -0
  19. package/assets/syntax/julia.yaml +56 -0
  20. package/assets/syntax/kotlin.yaml +65 -0
  21. package/assets/syntax/makefile.yaml +37 -0
  22. package/assets/syntax/nginx.yaml +22 -0
  23. package/assets/syntax/nim.yaml +27 -0
  24. package/assets/syntax/nix.yaml +32 -0
  25. package/assets/syntax/objc.yaml +60 -0
  26. package/assets/syntax/ocaml.yaml +43 -0
  27. package/assets/syntax/perl.yaml +58 -0
  28. package/assets/syntax/php.yaml +60 -0
  29. package/assets/syntax/r.yaml +30 -0
  30. package/assets/syntax/scala.yaml +32 -0
  31. package/assets/syntax/svelte.yaml +27 -0
  32. package/assets/syntax/swift.yaml +102 -0
  33. package/assets/syntax/vue.yaml +63 -0
  34. package/assets/syntax/xml.yaml +37 -0
  35. package/assets/syntax/zig.yaml +52 -0
  36. package/dist/claude-code.js +461 -149
  37. package/dist/cli.js +1490 -476
  38. package/dist/codex.js +461 -149
  39. package/dist/opencode.js +453 -142
  40. package/dist/pair-tui.js +172 -26
  41. package/dist/pi.js +508 -155
  42. package/hooks/codex.json +17 -0
  43. package/hooks/hooks.json +16 -0
  44. package/package.json +8 -3
  45. package/skills/toggle/SKILL.md +24 -0
@@ -7363,11 +7363,79 @@ 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/readPayload.ts
7385
+ import { readFileSync as readFileSync2 } from "node:fs";
7386
+ function readPayload() {
7387
+ const text = readFileSync2(0, "utf-8");
7388
+ return JSON.parse(text);
7389
+ }
7390
+
7391
+ // src/helpers/removeQuietly.ts
7392
+ import { unlinkSync } from "node:fs";
7393
+ function removeQuietly(path) {
7394
+ try {
7395
+ unlinkSync(path);
7396
+ } catch {
7397
+ }
7398
+ }
7399
+
7400
+ // src/helpers/resolvesOnPath.ts
7401
+ import { spawnSync } from "node:child_process";
7402
+ var defaultResolvesOnPath = (command) => {
7403
+ const result = spawnSync("which", [command], { stdio: "ignore" });
7404
+ return result.status === 0;
7405
+ };
7406
+
7407
+ // src/helpers/resultFilePath.ts
7408
+ import { randomBytes } from "node:crypto";
7409
+ import { tmpdir } from "node:os";
7410
+ import { join } from "node:path";
7411
+ var NAME_BYTES = 6;
7412
+ function resultFilePath() {
7413
+ const name = `pair-result-${randomBytes(NAME_BYTES).toString("hex")}.json`;
7414
+ return join(tmpdir(), name);
7415
+ }
7416
+
7417
+ // src/helpers/spawn.ts
7418
+ import { spawnSync as spawnSync2 } from "node:child_process";
7419
+ var defaultSpawn = (command, args) => {
7420
+ const result = spawnSync2(command, args, { stdio: ["ignore", "ignore", "pipe"] });
7421
+ const stderr = result.stderr ? result.stderr.toString("utf-8") : result.error?.message ?? "";
7422
+ return { status: result.status, stderr };
7423
+ };
7424
+
7425
+ // src/helpers/splitLines.ts
7426
+ function splitLines(text) {
7427
+ const lines = text.split("\n");
7428
+ const last = lines.at(-1);
7429
+ if (last === "") {
7430
+ lines.pop();
7431
+ }
7432
+ return lines;
7433
+ }
7434
+
7435
+ // src/core/state/state.ts
7368
7436
  function stateDir() {
7369
- const base = process.env["XDG_STATE_HOME"] || join(homedir(), ".local", "state");
7370
- return join(base, "pair-mode");
7437
+ const base = process.env["XDG_STATE_HOME"] || join2(homedir(), ".local", "state");
7438
+ return join2(base, "pair-mode");
7371
7439
  }
7372
7440
  function realpathLenient(path) {
7373
7441
  try {
@@ -7377,7 +7445,7 @@ function realpathLenient(path) {
7377
7445
  if (parent === path) {
7378
7446
  return path;
7379
7447
  }
7380
- return join(realpathLenient(parent), basename(path));
7448
+ return join2(realpathLenient(parent), basename(path));
7381
7449
  }
7382
7450
  }
7383
7451
  var DIGEST_LENGTH = 16;
@@ -7386,13 +7454,13 @@ function digestFor(directory) {
7386
7454
  return createHash("sha1").update(real).digest("hex").slice(0, DIGEST_LENGTH);
7387
7455
  }
7388
7456
  function flagPath(directory) {
7389
- return join(stateDir(), `${digestFor(directory)}.on`);
7457
+ return join2(stateDir(), `${digestFor(directory)}.on`);
7390
7458
  }
7391
7459
  function sessionsDir() {
7392
- return join(stateDir(), "sessions");
7460
+ return join2(stateDir(), "sessions");
7393
7461
  }
7394
7462
  function sessionSocketPath(directory) {
7395
- return join(sessionsDir(), `${digestFor(directory)}.sock`);
7463
+ return join2(sessionsDir(), `${digestFor(directory)}.sock`);
7396
7464
  }
7397
7465
  function findSessionSocket(filePath) {
7398
7466
  let current = dirname(realpathLenient(filePath));
@@ -7408,7 +7476,13 @@ function findSessionSocket(filePath) {
7408
7476
  current = parent;
7409
7477
  }
7410
7478
  }
7411
- function isEnabled(filePath) {
7479
+ function sessionFlagState(key) {
7480
+ if (existsSync(sessionKeyOptOutPath(key))) {
7481
+ return "off";
7482
+ }
7483
+ return existsSync(sessionKeyFlagPath(key)) ? "on" : "unset";
7484
+ }
7485
+ function directoryEnabled(filePath) {
7412
7486
  let current = dirname(realpathLenient(filePath));
7413
7487
  while (true) {
7414
7488
  if (existsSync(flagPath(current))) {
@@ -7421,10 +7495,34 @@ function isEnabled(filePath) {
7421
7495
  current = parent;
7422
7496
  }
7423
7497
  }
7424
-
7425
- // src/helpers/isRecord.ts
7426
- function isRecord(value) {
7427
- return typeof value === "object" && value !== null && !Array.isArray(value);
7498
+ function isEnabled(filePath, key) {
7499
+ if (key !== void 0) {
7500
+ const state = sessionFlagState(key);
7501
+ if (state !== "unset") {
7502
+ return state === "on";
7503
+ }
7504
+ }
7505
+ return directoryEnabled(filePath);
7506
+ }
7507
+ var SESSION_KEY_LENGTH = 8;
7508
+ function sessionKey(agentSessionId) {
7509
+ const digest = createHash("sha1").update(agentSessionId).digest("hex");
7510
+ return `s-${digest.slice(0, SESSION_KEY_LENGTH)}`;
7511
+ }
7512
+ function keyFor(agentSessionId) {
7513
+ return agentSessionId === void 0 || agentSessionId === "" ? void 0 : sessionKey(agentSessionId);
7514
+ }
7515
+ function sessionKeyPath(key, extension) {
7516
+ return join2(sessionsDir(), `${key}${extension}`);
7517
+ }
7518
+ function sessionKeySocketPath(key) {
7519
+ return sessionKeyPath(key, ".sock");
7520
+ }
7521
+ function sessionKeyFlagPath(key) {
7522
+ return sessionKeyPath(key, ".on");
7523
+ }
7524
+ function sessionKeyOptOutPath(key) {
7525
+ return sessionKeyPath(key, ".off");
7428
7526
  }
7429
7527
 
7430
7528
  // src/core/simulate/simulate.ts
@@ -7471,7 +7569,7 @@ function applyEdit(text, edit) {
7471
7569
  }
7472
7570
  return replaceFirst(text, old, next);
7473
7571
  }
7474
- function simulate(tool, input, readFile) {
7572
+ function simulate(tool, input, readFile, sessionId) {
7475
7573
  if (!isRecord(input)) {
7476
7574
  return null;
7477
7575
  }
@@ -7483,7 +7581,7 @@ function simulate(tool, input, readFile) {
7483
7581
  const content = input["content"];
7484
7582
  const after = typeof content === "string" ? content : "";
7485
7583
  const before = readFile(filePath);
7486
- return { tool, filePath, before, after };
7584
+ return { tool, filePath, before, after, sessionId };
7487
7585
  }
7488
7586
  if (tool === "Edit" || tool === "MultiEdit") {
7489
7587
  const editsRaw = input["edits"];
@@ -7498,7 +7596,7 @@ function simulate(tool, input, readFile) {
7498
7596
  if (after === null) {
7499
7597
  return null;
7500
7598
  }
7501
- return { tool, filePath, before, after };
7599
+ return { tool, filePath, before, after, sessionId };
7502
7600
  }
7503
7601
  return null;
7504
7602
  }
@@ -7929,16 +8027,6 @@ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, rmSync
7929
8027
  import { tmpdir as tmpdir2 } from "node:os";
7930
8028
  import { join as join8 } from "node:path";
7931
8029
 
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
8030
  // src/core/render/render.ts
7943
8031
  var HEADER_LEAD = [
7944
8032
  "# PAIR MODE. Left pane is the current file. Right pane is the proposal.",
@@ -7977,56 +8065,299 @@ function renderInline(input) {
7977
8065
  // src/editors/micro.ts
7978
8066
  var import_yaml = __toESM(require_dist(), 1);
7979
8067
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
7980
- import { join as join3 } from "node:path";
8068
+ import { join as join4 } from "node:path";
7981
8069
 
7982
8070
  // src/editors/languages.ts
7983
- import { extname } from "node:path";
7984
- var LANGS = {
8071
+ import { openSync, readSync, closeSync } from "node:fs";
8072
+ import { basename as basename2, extname } from "node:path";
8073
+ var EXTENSIONS = {
7985
8074
  ".go": "go",
7986
8075
  ".rb": "ruby",
7987
8076
  ".rake": "ruby",
8077
+ ".gemspec": "ruby",
8078
+ ".ru": "ruby",
8079
+ ".erb": "erb",
8080
+ ".haml": "haml",
7988
8081
  ".ts": "typescript",
7989
- ".tsx": "typescript",
8082
+ ".mts": "typescript",
8083
+ ".cts": "typescript",
8084
+ ".tsx": "tsx",
7990
8085
  ".js": "javascript",
7991
- ".jsx": "javascript",
7992
8086
  ".mjs": "javascript",
7993
- ".py": "python3",
8087
+ ".cjs": "javascript",
8088
+ ".jsx": "jsx",
8089
+ ".vue": "vue",
8090
+ ".svelte": "svelte",
8091
+ ".astro": "astro",
8092
+ ".py": "python",
8093
+ ".pyi": "python",
7994
8094
  ".ex": "elixir",
7995
8095
  ".exs": "elixir",
8096
+ ".heex": "elixir",
8097
+ ".erl": "erlang",
8098
+ ".hrl": "erlang",
7996
8099
  ".rs": "rust",
7997
- ".sh": "sh",
7998
- ".bash": "sh",
7999
- ".fish": "fish",
8100
+ ".sh": "shellscript",
8101
+ ".bash": "shellscript",
8102
+ ".ksh": "shellscript",
8000
8103
  ".zsh": "zsh",
8104
+ ".fish": "fish",
8105
+ ".ps1": "powershell",
8106
+ ".psm1": "powershell",
8107
+ ".bat": "bat",
8108
+ ".cmd": "bat",
8001
8109
  ".sql": "sql",
8002
8110
  ".json": "json",
8111
+ ".jsonc": "jsonc",
8112
+ ".json5": "json5",
8003
8113
  ".tf": "terraform",
8114
+ ".tfvars": "terraform",
8115
+ ".hcl": "hcl",
8004
8116
  ".proto": "proto",
8005
- ".dockerfile": "dockerfile",
8117
+ ".dockerfile": "docker",
8006
8118
  ".toml": "toml",
8007
8119
  ".yaml": "yaml",
8008
8120
  ".yml": "yaml",
8121
+ ".ini": "ini",
8122
+ ".cfg": "ini",
8123
+ ".properties": "properties",
8124
+ ".env": "dotenv",
8009
8125
  ".md": "markdown",
8126
+ ".markdown": "markdown",
8127
+ ".mdx": "mdx",
8010
8128
  ".css": "css",
8129
+ ".scss": "scss",
8130
+ ".sass": "sass",
8131
+ ".less": "less",
8132
+ ".styl": "stylus",
8011
8133
  ".html": "html",
8012
- ".erb": "html",
8134
+ ".htm": "html",
8135
+ ".xml": "xml",
8136
+ ".xsl": "xml",
8137
+ ".svg": "xml",
8013
8138
  ".lua": "lua",
8014
8139
  ".c": "c",
8015
- ".h": "c"
8140
+ ".h": "c",
8141
+ ".cc": "cpp",
8142
+ ".cpp": "cpp",
8143
+ ".cxx": "cpp",
8144
+ ".hpp": "cpp",
8145
+ ".hh": "cpp",
8146
+ ".cs": "csharp",
8147
+ ".java": "java",
8148
+ ".kt": "kotlin",
8149
+ ".kts": "kotlin",
8150
+ ".swift": "swift",
8151
+ ".m": "objective-c",
8152
+ ".mm": "objective-c",
8153
+ ".php": "php",
8154
+ ".pl": "perl",
8155
+ ".pm": "perl",
8156
+ ".scala": "scala",
8157
+ ".sc": "scala",
8158
+ ".clj": "clojure",
8159
+ ".cljs": "clojure",
8160
+ ".cljc": "clojure",
8161
+ ".hs": "haskell",
8162
+ ".nix": "nix",
8163
+ ".r": "r",
8164
+ ".jl": "julia",
8165
+ ".zig": "zig",
8166
+ ".nim": "nim",
8167
+ ".dart": "dart",
8168
+ ".groovy": "groovy",
8169
+ ".gradle": "groovy",
8170
+ ".cr": "crystal",
8171
+ ".ml": "ocaml",
8172
+ ".mli": "ocaml",
8173
+ ".fs": "fsharp",
8174
+ ".fsx": "fsharp",
8175
+ ".elm": "elm",
8176
+ ".graphql": "graphql",
8177
+ ".gql": "graphql",
8178
+ ".prisma": "prisma",
8179
+ ".sol": "solidity",
8180
+ ".vim": "viml",
8181
+ ".diff": "diff",
8182
+ ".patch": "diff",
8183
+ ".tex": "latex",
8184
+ ".wgsl": "wgsl",
8185
+ ".glsl": "glsl",
8186
+ ".cue": "cue"
8187
+ };
8188
+ var FILENAMES = {
8189
+ gemfile: "ruby",
8190
+ rakefile: "ruby",
8191
+ capfile: "ruby",
8192
+ vagrantfile: "ruby",
8193
+ guardfile: "ruby",
8194
+ podfile: "ruby",
8195
+ fastfile: "ruby",
8196
+ appfile: "ruby",
8197
+ brewfile: "ruby",
8198
+ "config.ru": "ruby",
8199
+ dockerfile: "docker",
8200
+ containerfile: "docker",
8201
+ makefile: "make",
8202
+ gnumakefile: "make",
8203
+ "cmakelists.txt": "cmake",
8204
+ "cargo.lock": "toml",
8205
+ "gemfile.lock": "toml",
8206
+ ".babelrc": "json",
8207
+ ".eslintrc": "json",
8208
+ ".prettierrc": "json",
8209
+ ".env": "dotenv",
8210
+ ".gitconfig": "ini",
8211
+ ".editorconfig": "ini",
8212
+ "nginx.conf": "nginx",
8213
+ "pnpm-workspace.yaml": "yaml"
8214
+ };
8215
+ var SHEBANGS = [
8216
+ { pattern: /^#!.*\/(env\s+)?ruby(\s|$)/, id: "ruby" },
8217
+ { pattern: /^#!.*\/(env\s+)?python[\d.]*(\s|$)/, id: "python" },
8218
+ { pattern: /^#!.*\/(env\s+)?(node|bun|deno)(\s|$)/, id: "javascript" },
8219
+ { pattern: /^#!.*\/(env\s+)?(bash|sh|ksh|dash)(\s|$)/, id: "shellscript" },
8220
+ { pattern: /^#!.*\/(env\s+)?zsh(\s|$)/, id: "zsh" },
8221
+ { pattern: /^#!.*\/(env\s+)?fish(\s|$)/, id: "fish" },
8222
+ { pattern: /^#!.*\/(env\s+)?perl(\s|$)/, id: "perl" },
8223
+ { pattern: /^#!.*\/(env\s+)?php(\s|$)/, id: "php" },
8224
+ { pattern: /^#!.*\/(env\s+)?(elixir|iex)(\s|$)/, id: "elixir" },
8225
+ { pattern: /^#!.*\/(env\s+)?lua(\s|$)/, id: "lua" },
8226
+ { pattern: /^#!.*\/(env\s+)?Rscript(\s|$)/, id: "r" },
8227
+ { pattern: /^#!.*\/(env\s+)?pwsh(\s|$)/, id: "powershell" }
8228
+ ];
8229
+ var FIRST_LINE_BYTES = 256;
8230
+ function firstLine(sourcePath) {
8231
+ let handle = null;
8232
+ try {
8233
+ handle = openSync(sourcePath, "r");
8234
+ const buffer = Buffer.alloc(FIRST_LINE_BYTES);
8235
+ const read = readSync(handle, buffer, 0, FIRST_LINE_BYTES, 0);
8236
+ const text = buffer.toString("utf-8", 0, read);
8237
+ const newline = text.indexOf("\n");
8238
+ return newline === -1 ? text : text.slice(0, newline);
8239
+ } catch {
8240
+ return null;
8241
+ } finally {
8242
+ if (handle !== null) {
8243
+ try {
8244
+ closeSync(handle);
8245
+ } catch {
8246
+ }
8247
+ }
8248
+ }
8249
+ }
8250
+ function shebangLanguage(sourcePath) {
8251
+ const line = firstLine(sourcePath);
8252
+ if (line === null || !line.startsWith("#!")) {
8253
+ return null;
8254
+ }
8255
+ return SHEBANGS.find((rule) => rule.pattern.test(line))?.id ?? null;
8256
+ }
8257
+ function detectLanguage(sourcePath) {
8258
+ const name = basename2(sourcePath).toLowerCase();
8259
+ const ext = extname(name);
8260
+ if (ext !== "" && EXTENSIONS[ext] !== void 0) {
8261
+ return EXTENSIONS[ext];
8262
+ }
8263
+ if (FILENAMES[name] !== void 0) {
8264
+ return FILENAMES[name];
8265
+ }
8266
+ return shebangLanguage(sourcePath);
8267
+ }
8268
+ var MICRO_SYNTAX = {
8269
+ c: "c",
8270
+ clojure: "clojure",
8271
+ cmake: "cmake",
8272
+ crystal: "crystal",
8273
+ csharp: "csharp",
8274
+ css: "css",
8275
+ dart: "dart",
8276
+ docker: "dockerfile",
8277
+ elixir: "elixir",
8278
+ elm: "elm",
8279
+ erb: "erb",
8280
+ erlang: "erlang",
8281
+ fish: "fish",
8282
+ fsharp: "fsharp",
8283
+ go: "go",
8284
+ graphql: "graphql",
8285
+ groovy: "groovy",
8286
+ haml: "haml",
8287
+ haskell: "haskell",
8288
+ html: "html",
8289
+ ini: "ini",
8290
+ java: "java",
8291
+ javascript: "javascript",
8292
+ json: "json",
8293
+ julia: "julia",
8294
+ kotlin: "kotlin",
8295
+ lua: "lua",
8296
+ make: "makefile",
8297
+ markdown: "markdown",
8298
+ nginx: "nginx",
8299
+ nim: "nim",
8300
+ nix: "nix",
8301
+ "objective-c": "objc",
8302
+ ocaml: "ocaml",
8303
+ perl: "perl",
8304
+ php: "php",
8305
+ proto: "proto",
8306
+ python: "python3",
8307
+ r: "r",
8308
+ ruby: "ruby",
8309
+ rust: "rust",
8310
+ scala: "scala",
8311
+ shellscript: "sh",
8312
+ sql: "sql",
8313
+ svelte: "svelte",
8314
+ swift: "swift",
8315
+ terraform: "terraform",
8316
+ toml: "toml",
8317
+ typescript: "typescript",
8318
+ vue: "vue",
8319
+ xml: "xml",
8320
+ yaml: "yaml",
8321
+ zig: "zig",
8322
+ zsh: "zsh"
8323
+ };
8324
+ var VIM_FILETYPES = {
8325
+ shellscript: "sh",
8326
+ docker: "dockerfile",
8327
+ csharp: "cs",
8328
+ "objective-c": "objc",
8329
+ bat: "dosbatch",
8330
+ viml: "vim",
8331
+ mdx: "markdown",
8332
+ jsx: "javascriptreact",
8333
+ tsx: "typescriptreact",
8334
+ dotenv: "sh",
8335
+ properties: "jproperties",
8336
+ stylus: "stylus"
8016
8337
  };
8017
- function syntaxName(sourcePath) {
8018
- const ext = extname(sourcePath).toLowerCase();
8019
- return LANGS[ext] ?? null;
8338
+ function microSyntaxName(sourcePath) {
8339
+ const id = detectLanguage(sourcePath);
8340
+ if (id === null) {
8341
+ return null;
8342
+ }
8343
+ return MICRO_SYNTAX[id] ?? null;
8344
+ }
8345
+ function vimFiletype(sourcePath) {
8346
+ const id = detectLanguage(sourcePath);
8347
+ if (id === null) {
8348
+ return null;
8349
+ }
8350
+ return VIM_FILETYPES[id] ?? id;
8020
8351
  }
8021
8352
 
8022
8353
  // 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";
8354
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
8355
+ import { dirname as dirname2, join as join3 } from "node:path";
8025
8356
  import { fileURLToPath } from "node:url";
8026
8357
  function defaultAssetsDir() {
8027
8358
  let dir = dirname2(fileURLToPath(import.meta.url));
8028
8359
  while (true) {
8029
- const candidate = join2(dir, "assets", "syntax");
8360
+ const candidate = join3(dir, "assets", "syntax");
8030
8361
  if (existsSync2(candidate)) {
8031
8362
  return candidate;
8032
8363
  }
@@ -8041,20 +8372,13 @@ function syntaxSource(lang, assetsDir = defaultAssetsDir()) {
8041
8372
  if (assetsDir === null) {
8042
8373
  return null;
8043
8374
  }
8044
- const path = join2(assetsDir, `${lang}.yaml`);
8375
+ const path = join3(assetsDir, `${lang}.yaml`);
8045
8376
  if (!existsSync2(path)) {
8046
8377
  return null;
8047
8378
  }
8048
- return readFileSync(path, "utf-8");
8379
+ return readFileSync3(path, "utf-8");
8049
8380
  }
8050
8381
 
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
8382
  // src/editors/micro.ts
8059
8383
  var MICRO_BINDINGS = {
8060
8384
  F2: "QuitAll",
@@ -8085,7 +8409,7 @@ function bandRules() {
8085
8409
  ];
8086
8410
  }
8087
8411
  function writeColorScheme(configDir, theme) {
8088
- const dir = join3(configDir, "colorschemes");
8412
+ const dir = join4(configDir, "colorschemes");
8089
8413
  mkdirSync2(dir, { recursive: true });
8090
8414
  const text = `include "monokai"
8091
8415
 
@@ -8093,7 +8417,7 @@ color-link pairadd "#d7ffd7,${theme.add}"
8093
8417
  color-link pairdel "#ffd7d7,${theme.del}"
8094
8418
  color-link pairskip "#6a6a6a,${theme.fold}"
8095
8419
  `;
8096
- writeFileSync2(join3(dir, "pair.micro"), text, "utf-8");
8420
+ writeFileSync2(join4(dir, "pair.micro"), text, "utf-8");
8097
8421
  }
8098
8422
  function syntaxText(lang, source) {
8099
8423
  const parsed = (0, import_yaml.parse)(source);
@@ -8109,7 +8433,7 @@ function syntaxText(lang, source) {
8109
8433
  return (0, import_yaml.stringify)(file, { lineWidth: 0 });
8110
8434
  }
8111
8435
  function writeSyntax(configDir, sourcePath) {
8112
- const lang = syntaxName(sourcePath);
8436
+ const lang = microSyntaxName(sourcePath);
8113
8437
  if (lang === null) {
8114
8438
  return;
8115
8439
  }
@@ -8121,9 +8445,9 @@ function writeSyntax(configDir, sourcePath) {
8121
8445
  if (text === null) {
8122
8446
  return;
8123
8447
  }
8124
- const dir = join3(configDir, "syntax");
8448
+ const dir = join4(configDir, "syntax");
8125
8449
  mkdirSync2(dir, { recursive: true });
8126
- writeFileSync2(join3(dir, `pair-${lang}.yaml`), text, "utf-8");
8450
+ writeFileSync2(join4(dir, `pair-${lang}.yaml`), text, "utf-8");
8127
8451
  }
8128
8452
  function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8129
8453
  return {
@@ -8136,7 +8460,7 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8136
8460
  return ["# F3 moves between panes. Ctrl+W or F2 sends and closes."];
8137
8461
  },
8138
8462
  bufferSuffix(sourcePath) {
8139
- const lang = syntaxName(sourcePath);
8463
+ const lang = microSyntaxName(sourcePath);
8140
8464
  if (lang === null) {
8141
8465
  return ".diff";
8142
8466
  }
@@ -8145,12 +8469,12 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8145
8469
  prepare(context) {
8146
8470
  mkdirSync2(context.configDir, { recursive: true });
8147
8471
  writeFileSync2(
8148
- join3(context.configDir, "bindings.json"),
8472
+ join4(context.configDir, "bindings.json"),
8149
8473
  JSON.stringify(MICRO_BINDINGS, null, 2),
8150
8474
  "utf-8"
8151
8475
  );
8152
8476
  writeFileSync2(
8153
- join3(context.configDir, "settings.json"),
8477
+ join4(context.configDir, "settings.json"),
8154
8478
  JSON.stringify(MICRO_SETTINGS, null, 2),
8155
8479
  "utf-8"
8156
8480
  );
@@ -8174,16 +8498,6 @@ function isHexColor(value) {
8174
8498
  }
8175
8499
 
8176
8500
  // 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
8501
  function safeThemeColor(value) {
8188
8502
  if (!isHexColor(value)) {
8189
8503
  throw new Error(`invalid theme colour for vim highlight: ${value}`);
@@ -8238,7 +8552,7 @@ function vimEditor(name, resolvesOnPath = defaultResolvesOnPath) {
8238
8552
 
8239
8553
  // src/editors/nano.ts
8240
8554
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
8241
- import { join as join4 } from "node:path";
8555
+ import { join as join5 } from "node:path";
8242
8556
  function safeThemeColor2(value) {
8243
8557
  if (!isHexColor(value)) {
8244
8558
  throw new Error(`invalid theme colour for nano rcfile: ${value}`);
@@ -8250,7 +8564,7 @@ function writeNanorc(configDir, theme) {
8250
8564
  color ,${safeThemeColor2(theme.del)} "^\u258C\u258C-"
8251
8565
  color ,${safeThemeColor2(theme.fold)} "^\u22EF"
8252
8566
  `;
8253
- const path = join4(configDir, "pair.nanorc");
8567
+ const path = join5(configDir, "pair.nanorc");
8254
8568
  writeFileSync3(path, text, "utf-8");
8255
8569
  return path;
8256
8570
  }
@@ -8280,13 +8594,13 @@ function createNanoEditor(resolvesOnPath = defaultResolvesOnPath) {
8280
8594
 
8281
8595
  // src/editors/pair.ts
8282
8596
  import { existsSync as existsSync4 } from "node:fs";
8283
- import { dirname as dirname4, extname as extname3, join as join6 } from "node:path";
8597
+ import { dirname as dirname4, extname as extname3, join as join7 } from "node:path";
8284
8598
  import { fileURLToPath as fileURLToPath2 } from "node:url";
8285
8599
 
8286
8600
  // src/core/config/config.ts
8287
8601
  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";
8602
+ import { join as join6, dirname as dirname3 } from "node:path";
8603
+ import { existsSync as existsSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "node:fs";
8290
8604
  var EDITOR_NAMES = ["auto", "pair", "micro", "nvim", "vim", "nano"];
8291
8605
  var MULTIPLEXER_NAMES = ["auto", "zellij", "tmux", "none"];
8292
8606
  var TRANSPORT_NAMES = ["pane", "session"];
@@ -8312,8 +8626,8 @@ var DEFAULT_CONFIG = {
8312
8626
  syntax: true
8313
8627
  };
8314
8628
  function configPath(homeDir) {
8315
- const base = process.env["XDG_CONFIG_HOME"] || join5(homeDir ?? homedir2(), ".config");
8316
- return join5(base, "pair-mode", "config.json");
8629
+ const base = process.env["XDG_CONFIG_HOME"] || join6(homeDir ?? homedir2(), ".config");
8630
+ return join6(base, "pair-mode", "config.json");
8317
8631
  }
8318
8632
  function isEditorName(value) {
8319
8633
  return typeof value === "string" && EDITOR_NAMES.includes(value);
@@ -8585,7 +8899,7 @@ function loadConfig(path) {
8585
8899
  }
8586
8900
  let text;
8587
8901
  try {
8588
- text = readFileSync2(target, "utf-8");
8902
+ text = readFileSync4(target, "utf-8");
8589
8903
  } catch (error) {
8590
8904
  const message = error instanceof Error ? error.message : String(error);
8591
8905
  return {
@@ -8620,7 +8934,7 @@ function paintLayout(layout) {
8620
8934
  // src/editors/pair.ts
8621
8935
  var defaultResolveTuiEntry = () => {
8622
8936
  const here = dirname4(fileURLToPath2(import.meta.url));
8623
- return join6(here, "pair-tui.js");
8937
+ return join7(here, "pair-tui.js");
8624
8938
  };
8625
8939
  var defaultCheckBundleExists = existsSync4;
8626
8940
  function createPairEditor(resolveTuiEntry = defaultResolveTuiEntry, checkBundleExists = defaultCheckBundleExists) {
@@ -8719,14 +9033,6 @@ function resolve(preference, resolvesOnPath = defaultResolvesOnPath, checkPairBu
8719
9033
  return available ?? vimEditor("vim", resolvesOnPath);
8720
9034
  }
8721
9035
 
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
9036
  // src/multiplexers/zellij.ts
8731
9037
  function createZellijMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultResolvesOnPath) {
8732
9038
  return {
@@ -8756,7 +9062,7 @@ function createZellijMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultR
8756
9062
  }
8757
9063
 
8758
9064
  // src/multiplexers/tmux.ts
8759
- import { randomBytes } from "node:crypto";
9065
+ import { randomBytes as randomBytes2 } from "node:crypto";
8760
9066
  var CHANNEL_BYTES = 8;
8761
9067
  var SAFE_UNQUOTED = /^[A-Za-z0-9_\-./]+$/;
8762
9068
  function shellQuote(value) {
@@ -8772,7 +9078,7 @@ function createTmuxMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultRes
8772
9078
  return Boolean(process.env["TMUX"]) && resolvesOnPath("tmux");
8773
9079
  },
8774
9080
  run(argv, size) {
8775
- const channel = `pair-${randomBytes(CHANNEL_BYTES).toString("hex")}`;
9081
+ const channel = `pair-${randomBytes2(CHANNEL_BYTES).toString("hex")}`;
8776
9082
  const inner = argv.map(shellQuote).join(" ");
8777
9083
  const script = `stty -ixon 2>/dev/null; ${inner}; tmux wait-for -S ${channel}`;
8778
9084
  const popup = spawn("tmux", [
@@ -8794,9 +9100,9 @@ function createTmuxMultiplexer(spawn = defaultSpawn, resolvesOnPath = defaultRes
8794
9100
  }
8795
9101
 
8796
9102
  // src/multiplexers/tty.ts
8797
- import { openSync, closeSync } from "node:fs";
9103
+ import { openSync as openSync2, closeSync as closeSync2 } from "node:fs";
8798
9104
  import { spawnSync as spawnSync3 } from "node:child_process";
8799
- var defaultOpen = () => openSync("/dev/tty", "r+");
9105
+ var defaultOpen = () => openSync2("/dev/tty", "r+");
8800
9106
  var defaultRunner = (command, args, fd) => {
8801
9107
  const result = spawnSync3(command, args, { stdio: [fd, fd, fd] });
8802
9108
  const detail = result.status === 0 ? "" : String(result.error?.message ?? result.stderr ?? "");
@@ -8804,7 +9110,7 @@ var defaultRunner = (command, args, fd) => {
8804
9110
  };
8805
9111
  function closeQuietly(fd) {
8806
9112
  try {
8807
- closeSync(fd);
9113
+ closeSync2(fd);
8808
9114
  } catch {
8809
9115
  }
8810
9116
  }
@@ -8861,42 +9167,6 @@ function detect(preference, adapters = {}) {
8861
9167
  return tty;
8862
9168
  }
8863
9169
 
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/readPayload.ts
8875
- import { readFileSync as readFileSync4 } from "node:fs";
8876
- function readPayload() {
8877
- const text = readFileSync4(0, "utf-8");
8878
- return JSON.parse(text);
8879
- }
8880
-
8881
- // src/helpers/removeQuietly.ts
8882
- import { unlinkSync as unlinkSync2 } from "node:fs";
8883
- function removeQuietly(path) {
8884
- try {
8885
- unlinkSync2(path);
8886
- } catch {
8887
- }
8888
- }
8889
-
8890
- // src/helpers/resultFilePath.ts
8891
- import { randomBytes as randomBytes2 } from "node:crypto";
8892
- import { tmpdir } from "node:os";
8893
- import { join as join7 } from "node:path";
8894
- var NAME_BYTES = 6;
8895
- function resultFilePath() {
8896
- const name = `pair-result-${randomBytes2(NAME_BYTES).toString("hex")}.json`;
8897
- return join7(tmpdir(), name);
8898
- }
8899
-
8900
9170
  // src/transports/pane/pane.ts
8901
9171
  var NAME_BYTES2 = 6;
8902
9172
  function splitCommand(value) {
@@ -8995,6 +9265,7 @@ function createPaneTransport(deps = {}) {
8995
9265
 
8996
9266
  // src/transports/session/client.ts
8997
9267
  import { createConnection } from "node:net";
9268
+ import { existsSync as existsSync5 } from "node:fs";
8998
9269
 
8999
9270
  // src/transports/session/wire.ts
9000
9271
  var CLIENT_KINDS = ["tui", "web"];
@@ -9009,11 +9280,11 @@ function createLineReader() {
9009
9280
  return parts.filter((line) => line.trim() !== "");
9010
9281
  };
9011
9282
  }
9012
- function isString(value) {
9283
+ function isString2(value) {
9013
9284
  return typeof value === "string";
9014
9285
  }
9015
9286
  function isClientKind(value) {
9016
- return isString(value) && CLIENT_KINDS.includes(value);
9287
+ return isString2(value) && CLIENT_KINDS.includes(value);
9017
9288
  }
9018
9289
  function isQuestion(value) {
9019
9290
  if (!isRecord(value)) {
@@ -9021,16 +9292,16 @@ function isQuestion(value) {
9021
9292
  }
9022
9293
  const line = value["line"];
9023
9294
  const lineOk = line === null || typeof line === "number";
9024
- return lineOk && isString(value["code"]) && isString(value["text"]);
9295
+ return lineOk && isString2(value["code"]) && isString2(value["text"]);
9025
9296
  }
9026
9297
  function isQuestionList(value) {
9027
9298
  return Array.isArray(value) && value.every(isQuestion);
9028
9299
  }
9029
9300
  function toSubmit(raw) {
9030
- if (!isString(raw["tool"]) || !isString(raw["path"])) {
9301
+ if (!isString2(raw["tool"]) || !isString2(raw["path"])) {
9031
9302
  return null;
9032
9303
  }
9033
- if (!isString(raw["before"]) || !isString(raw["after"])) {
9304
+ if (!isString2(raw["before"]) || !isString2(raw["after"])) {
9034
9305
  return null;
9035
9306
  }
9036
9307
  return {
@@ -9046,7 +9317,7 @@ function toAttach(raw) {
9046
9317
  }
9047
9318
  function toReview(raw) {
9048
9319
  const submit = toSubmit({ ...raw, type: "submit" });
9049
- if (submit === null || !isString(raw["id"])) {
9320
+ if (submit === null || !isString2(raw["id"])) {
9050
9321
  return null;
9051
9322
  }
9052
9323
  return {
@@ -9059,13 +9330,31 @@ function toReview(raw) {
9059
9330
  };
9060
9331
  }
9061
9332
  function toVerdict(raw) {
9062
- if (!isString(raw["id"]) || !isQuestionList(raw["questions"])) {
9333
+ if (!isString2(raw["id"]) || !isQuestionList(raw["questions"])) {
9063
9334
  return null;
9064
9335
  }
9065
9336
  return { type: "verdict", id: raw["id"], questions: raw["questions"] };
9066
9337
  }
9067
9338
  function toCancel(raw) {
9068
- return isString(raw["id"]) ? { type: "cancel", id: raw["id"] } : null;
9339
+ return isString2(raw["id"]) ? { type: "cancel", id: raw["id"] } : null;
9340
+ }
9341
+ function isNumber(value) {
9342
+ return typeof value === "number" && Number.isFinite(value);
9343
+ }
9344
+ function toState(raw) {
9345
+ const lastAttachAt = raw["lastAttachAt"];
9346
+ if (lastAttachAt !== null && !isString2(lastAttachAt)) {
9347
+ return null;
9348
+ }
9349
+ if (!isNumber(raw["clientCount"]) || !isNumber(raw["waitingDepth"])) {
9350
+ return null;
9351
+ }
9352
+ return {
9353
+ type: "state",
9354
+ clientCount: raw["clientCount"],
9355
+ waitingDepth: raw["waitingDepth"],
9356
+ lastAttachAt
9357
+ };
9069
9358
  }
9070
9359
  function decodeLine(line) {
9071
9360
  let parsed;
@@ -9093,11 +9382,18 @@ function decodeLine(line) {
9093
9382
  if (type === "cancel") {
9094
9383
  return toCancel(parsed);
9095
9384
  }
9385
+ if (type === "status") {
9386
+ return { type: "status" };
9387
+ }
9388
+ if (type === "state") {
9389
+ return toState(parsed);
9390
+ }
9096
9391
  return null;
9097
9392
  }
9098
9393
 
9099
9394
  // src/transports/session/client.ts
9100
9395
  var MS_PER_SECOND = 1e3;
9396
+ var NO_WATCHER = "no pair-mode watcher attached";
9101
9397
  function failOpen(detail) {
9102
9398
  return { reviewed: false, detail };
9103
9399
  }
@@ -9130,7 +9426,7 @@ function requestReview(request, options) {
9130
9426
  let lastError = "";
9131
9427
  socket.on("error", (error) => {
9132
9428
  if (isConnectionRefused(error)) {
9133
- settle(failOpen("no pair-mode watcher attached"));
9429
+ settle(failOpen(NO_WATCHER));
9134
9430
  return;
9135
9431
  }
9136
9432
  lastError = error instanceof Error ? error.message : String(error);
@@ -9160,15 +9456,27 @@ function requestReview(request, options) {
9160
9456
  });
9161
9457
  });
9162
9458
  }
9163
- function reviewInSession(request, config, socketPath) {
9164
- const path = socketPath ?? findSessionSocket(request.filePath);
9165
- if (path === null) {
9166
- return Promise.resolve(failOpen("no pair-mode watcher attached"));
9459
+ async function reviewInSession(request, config, socketPath) {
9460
+ const timeoutMs = config.session.timeout * MS_PER_SECOND;
9461
+ if (socketPath !== void 0) {
9462
+ return await requestReview(request, { socketPath, timeoutMs });
9167
9463
  }
9168
- return requestReview(request, {
9169
- socketPath: path,
9170
- timeoutMs: config.session.timeout * MS_PER_SECOND
9171
- });
9464
+ const key = keyFor(request.sessionId);
9465
+ const sessionPath = key === void 0 ? null : sessionKeySocketPath(key);
9466
+ if (sessionPath !== null && existsSync5(sessionPath)) {
9467
+ const outcome = await requestReview(request, { socketPath: sessionPath, timeoutMs });
9468
+ if (outcome.reviewed) {
9469
+ return outcome;
9470
+ }
9471
+ if (outcome.detail !== NO_WATCHER) {
9472
+ return outcome;
9473
+ }
9474
+ }
9475
+ const directoryPath = findSessionSocket(request.filePath);
9476
+ if (directoryPath === null) {
9477
+ return failOpen(NO_WATCHER);
9478
+ }
9479
+ return await requestReview(request, { socketPath: directoryPath, timeoutMs });
9172
9480
  }
9173
9481
  function createSessionTransport(socketPath) {
9174
9482
  return {
@@ -9189,7 +9497,8 @@ async function runPair(request, config, deps = {}) {
9189
9497
  if (request.before === request.after) {
9190
9498
  return { decision: "allow", reviewed: false };
9191
9499
  }
9192
- if (!isEnabled(request.filePath)) {
9500
+ const key = keyFor(request.sessionId);
9501
+ if (!isEnabled(request.filePath, key)) {
9193
9502
  return { decision: "allow", reviewed: false };
9194
9503
  }
9195
9504
  const transport = deps.transport ?? resolveTransport(config, deps);
@@ -9245,15 +9554,18 @@ async function main(config) {
9245
9554
  if (!isRecord(toolInput)) {
9246
9555
  return 0;
9247
9556
  }
9557
+ const rawSessionId = payload["session_id"];
9558
+ const sessionId = typeof rawSessionId === "string" ? rawSessionId : void 0;
9248
9559
  const filePathValue = toolInput["file_path"];
9249
9560
  const filePath = typeof filePathValue === "string" ? filePathValue : "";
9250
9561
  if (filePath === "") {
9251
9562
  return 0;
9252
9563
  }
9253
- if (!isEnabled(filePath)) {
9564
+ const key = keyFor(sessionId);
9565
+ if (!isEnabled(filePath, key)) {
9254
9566
  return 0;
9255
9567
  }
9256
- const request = simulate(tool, toolInput, readFileOrEmpty);
9568
+ const request = simulate(tool, toolInput, readFileOrEmpty, sessionId);
9257
9569
  if (request === null) {
9258
9570
  trace("exit: could not simulate", config);
9259
9571
  return 0;