@ree-auz/nook 0.1.1 → 0.1.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.
@@ -6,7 +6,7 @@ issue's branch (running the repository's hooks) and starts your agent in it
6
6
  with Linear's prompt, in the terminal Linear opens for the script. Nook
7
7
  itself knows nothing about Linear: this folder only connects the two.
8
8
 
9
- Tested on Windows. `--terminal` (Windows Terminal or Warp) is Windows only.
9
+ Tested on Windows. `--terminal wt` and `--terminal warp` are Windows only.
10
10
 
11
11
  ## Set up once
12
12
 
@@ -42,6 +42,15 @@ Tested on Windows. `--terminal` (Windows Terminal or Warp) is Windows only.
42
42
  Terminal; the terminal choice in Linear's settings is macOS only). To
43
43
  work in Warp instead, add `"--terminal", "warp"` to `args`: the script
44
44
  opens a tab in Warp and closes Linear's window.
45
+ - To keep agents in [Orca](https://www.onorca.dev) (its accounts and
46
+ sessions), add `"--terminal", "orca"`: Nook creates the workspace and
47
+ runs its hooks in Linear's window, then
48
+ `orca agent launch --agent <agent> --worktree path:<workspace> --prompt <Linear's prompt> --focus`
49
+ starts the agent in Orca. Orca shows worktrees it did not create, and
50
+ `nk rm` / `nk clean` still remove them with their containers and
51
+ volumes. Orca's command-line tool must be installed; if Linear cannot
52
+ find it on PATH, add `"--orca", "<full path to orca>"`. Linear's window
53
+ closes when Orca has the agent, and stays open with the error otherwise.
45
54
  - To let each issue pick its agent, label issues from one label group
46
55
  (say `Agent › Codex`, `Agent › Claude`) and add `"--agent-label", "Agent"`
47
56
  to `args`. Linear's prompt lists the issue's labels, and the script starts
@@ -59,7 +68,8 @@ the issue belongs to; nothing runs there.
59
68
  The terminal runs `nk -b <issue branch> <agent> -y -p <Linear's prompt>` in
60
69
  that folder (`-y` keeps the converted workspace name without asking).
61
70
  Choosing the same issue again reopens its workspace and starts the agent
62
- without the prompt.
71
+ without the prompt (with `--terminal orca`, a new agent session in Orca;
72
+ earlier sessions stay in Orca's list).
63
73
 
64
74
  Linear's GitHub integration moves the issue along as the branch and its pull
65
75
  request change, because the branch name holds the issue identifier. After the
@@ -3,14 +3,16 @@
3
3
  // Linear runs this file through ~/.linear/coding-tools.json
4
4
  // (Work on issue -> Custom script). See README.md next to it.
5
5
  //
6
- // node open-issue.mjs [agent] started by Linear (no window)
6
+ // node open-issue.mjs [--agent a] [--terminal wt|warp|orca] started by Linear
7
7
  // node open-issue.mjs --run <file> ... inside the new tab
8
8
  import { spawn, spawnSync } from "node:child_process";
9
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { homedir, tmpdir } from "node:os";
11
11
  import { delimiter, dirname, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { agentCommand, windowsDirect } from "../../src/agents.mjs";
13
14
  import { normalizeName } from "../../src/names.mjs";
15
+ import crossSpawn from "cross-spawn";
14
16
 
15
17
  const self = fileURLToPath(import.meta.url);
16
18
  const nk = fileURLToPath(new URL("../../bin/nk.mjs", import.meta.url));
@@ -30,10 +32,15 @@ function runNk(args, opts = {}) {
30
32
  return spawnSync(process.execPath, [nk, ...args], { stdio: "inherit", env: withNodeOnPath(), ...opts });
31
33
  }
32
34
 
35
+ /** The issue's workspace from `nk ls --json`, if it exists. */
36
+ function findWorkspace(branch) {
37
+ const listed = spawnSync(process.execPath, [nk, "ls", "--json"], { encoding: "utf8", env: withNodeOnPath() });
38
+ return listed.status === 0 ? JSON.parse(listed.stdout).find((ws) => !ws.main && (ws.branch === branch || ws.name === normalizeName(branch))) : null;
39
+ }
40
+
33
41
  /** Creates the issue's workspace and starts the agent, or reopens it. */
34
42
  function startWork({ branch, agent, prompt }) {
35
- const listed = spawnSync(process.execPath, [nk, "ls", "--json"], { encoding: "utf8", env: withNodeOnPath() });
36
- const existing = listed.status === 0 ? JSON.parse(listed.stdout).find((ws) => !ws.main && (ws.branch === branch || ws.name === normalizeName(branch))) : null;
43
+ const existing = findWorkspace(branch);
37
44
  // Clicked again for the same issue: reopen the workspace without restarting the task.
38
45
  // -y: the branch name Linear suggests is converted without asking.
39
46
  const result = existing ? runNk([existing.name, agent]) : runNk(["-b", branch, agent, "-y", "-p", prompt]);
@@ -91,6 +98,38 @@ function which(command) {
91
98
 
92
99
  const TERMINALS = { wt: openInWindowsTerminal, warp: openInWarp };
93
100
 
101
+ /**
102
+ * Orca: nk creates the workspace (hooks, ports, database), then Orca starts
103
+ * the agent in it, so Orca keeps its accounts and sessions. Orca shows
104
+ * worktrees it did not create. A new workspace gets Linear's prompt; one
105
+ * that exists only gets a new agent session.
106
+ */
107
+ export function workInOrca({ branch, agent, prompt, orca = "orca", run = runOrca }) {
108
+ const existing = findWorkspace(branch);
109
+ if (!existing) {
110
+ const created = runNk(["-b", branch, "-y"]);
111
+ if (created.status !== 0) return created.status ?? 1;
112
+ }
113
+ const ws = existing ?? findWorkspace(branch);
114
+ if (!ws) throw new Error(`nk created no workspace for ${branch}`);
115
+ run(orca, ["open", "--json"]);
116
+ const args = ["agent", "launch", "--agent", agent, "--worktree", `path:${ws.path}`, "--focus", "--json"];
117
+ if (!existing && prompt) args.push("--prompt", prompt);
118
+ return run(orca, args);
119
+ }
120
+
121
+ /** Runs the Orca CLI, never through cmd.exe, which would cut the prompt. */
122
+ function runOrca(orca, args) {
123
+ const env = withNodeOnPath();
124
+ const spec = windowsDirect({ command: agentCommand({}, orca, {}, env).command, args });
125
+ // cross-spawn: a .cmd shim still runs when the arguments have no line breaks.
126
+ const result = crossSpawn.sync(spec.command, spec.args, { stdio: ["ignore", "pipe", "inherit"], encoding: "utf8", env });
127
+ if (result.error) throw new Error(`could not run ${orca}: ${result.error.message}. Is Orca's command-line tool installed? Pass its path with --orca`);
128
+ // Orca answers in JSON; show it only when something went wrong.
129
+ if (result.status !== 0) process.stdout.write(result.stdout ?? "");
130
+ return result.status ?? 1;
131
+ }
132
+
94
133
  /**
95
134
  * Closes the terminal window Linear opened for this script, once the work
96
135
  * runs in another terminal: its shell is this process's parent.
@@ -130,8 +169,15 @@ function fromLinear(argv) {
130
169
  process.chdir(dir);
131
170
  return startWork({ branch, agent, prompt });
132
171
  }
172
+ if (terminal === "orca") {
173
+ process.chdir(dir);
174
+ process.exitCode = workInOrca({ branch, agent, prompt, orca: option(argv, "--orca") ?? "orca" });
175
+ // Keep Linear's window open when something failed, so its output can be read.
176
+ if (process.exitCode === 0 && process.platform === "win32") closeLinearWindow();
177
+ return;
178
+ }
133
179
  const open = TERMINALS[terminal];
134
- if (!open) throw new Error(`Unknown terminal "${terminal}"; use ${Object.keys(TERMINALS).join(" or ")}, or leave --terminal out`);
180
+ if (!open) throw new Error(`Unknown terminal "${terminal}"; use ${[...Object.keys(TERMINALS), "orca"].join(", ")}, or leave --terminal out`);
135
181
  if (process.platform !== "win32") throw new Error("--terminal only works on Windows so far");
136
182
  // The prompt is long Markdown with quotes and semicolons, so it travels in a file.
137
183
  const file = join(tmpdir(), `nk-linear-${id}-${process.pid}.md`);
@@ -140,7 +186,20 @@ function fromLinear(argv) {
140
186
  closeLinearWindow();
141
187
  }
142
188
 
143
- if (process.argv[1] && resolve(process.argv[1]) === self) {
189
+ /**
190
+ * Run directly, not imported. Node gives this file its real path, so a path
191
+ * through a link (fnm's aliases\default, for one) is resolved before comparing.
192
+ */
193
+ function runDirectly() {
194
+ if (!process.argv[1]) return false;
195
+ try {
196
+ return realpathSync(resolve(process.argv[1])) === realpathSync(self);
197
+ } catch {
198
+ return resolve(process.argv[1]) === self;
199
+ }
200
+ }
201
+
202
+ if (runDirectly()) {
144
203
  const [first, ...rest] = process.argv.slice(2);
145
204
  if (first === "--run") insideTab(rest);
146
205
  else fromLinear(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ree-auz/nook",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "One home folder for your git worktrees: create, open and remove workspaces with shared files, ports and hooks.",
5
5
  "license": "MIT",
6
6
  "type": "module",