nomoreide 0.1.89 → 0.1.91

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 (48) hide show
  1. package/README.md +46 -24
  2. package/dist/core/agent-terminal.d.ts +9 -1
  3. package/dist/core/agent-terminal.js +26 -5
  4. package/dist/core/agent-terminal.js.map +1 -1
  5. package/dist/core/agent-transcripts.d.ts +27 -0
  6. package/dist/core/agent-transcripts.js +240 -0
  7. package/dist/core/agent-transcripts.js.map +1 -0
  8. package/dist/core/config-store.d.ts +18 -0
  9. package/dist/core/config-store.js +53 -0
  10. package/dist/core/config-store.js.map +1 -1
  11. package/dist/core/docker-inspect.d.ts +34 -0
  12. package/dist/core/docker-inspect.js +108 -0
  13. package/dist/core/docker-inspect.js.map +1 -0
  14. package/dist/core/docker-resources.d.ts +28 -0
  15. package/dist/core/docker-resources.js +59 -0
  16. package/dist/core/docker-resources.js.map +1 -0
  17. package/dist/core/docker-stats.d.ts +22 -0
  18. package/dist/core/docker-stats.js +52 -0
  19. package/dist/core/docker-stats.js.map +1 -0
  20. package/dist/core/docker.d.ts +15 -0
  21. package/dist/core/docker.js +38 -1
  22. package/dist/core/docker.js.map +1 -1
  23. package/dist/core/repo-create.d.ts +11 -0
  24. package/dist/core/repo-create.js +54 -0
  25. package/dist/core/repo-create.js.map +1 -0
  26. package/dist/core/terminal-manager.d.ts +2 -0
  27. package/dist/core/terminal-manager.js +6 -0
  28. package/dist/core/terminal-manager.js.map +1 -1
  29. package/dist/core/terminal-session.d.ts +9 -1
  30. package/dist/core/terminal-session.js +24 -1
  31. package/dist/core/terminal-session.js.map +1 -1
  32. package/dist/core/types.d.ts +6 -0
  33. package/dist/web/client/assets/SymbolsNerdFontMono-Regular-DDncdh2F.ttf +0 -0
  34. package/dist/web/client/assets/{code-editor-Cw2LhseL.js → code-editor-CRI31ZMh.js} +1 -1
  35. package/dist/web/client/assets/index-CQU15q2M.css +1 -0
  36. package/dist/web/client/assets/index-hu3LqMhU.js +228 -0
  37. package/dist/web/client/index.html +2 -2
  38. package/dist/web/routes/docker-routes.js +29 -2
  39. package/dist/web/routes/docker-routes.js.map +1 -1
  40. package/dist/web/routes/git-routes.js +36 -0
  41. package/dist/web/routes/git-routes.js.map +1 -1
  42. package/dist/web/routes/service-routes.js +21 -0
  43. package/dist/web/routes/service-routes.js.map +1 -1
  44. package/dist/web/routes/terminal-routes.js +37 -7
  45. package/dist/web/routes/terminal-routes.js.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/web/client/assets/index-D4SJDKDx.js +0 -228
  48. package/dist/web/client/assets/index-D971Mghq.css +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="assets/nomoreide-logo.png" alt="NoMoreIDE Logo" width="120" />
3
+ <img src="assets/nomoreide-logo.svg" alt="NoMoreIDE Logo" width="120" />
4
4
 
5
5
  # NoMoreIDE
6
6
 
@@ -15,7 +15,21 @@
15
15
 
16
16
  Give your coding agents and yourself a **shared local control surface** for services, ports, logs, Git review, GitHub workflows, database work, and MCP workflows — no IDE required.
17
17
 
18
- [MCP Setup](#connect-your-ai-agent) · [CLI Reference](#cli) · [MCP Tools](#mcp-tools) · [Architecture](#architecture)
18
+ [Product Tour](#product-tour) · [Download for macOS](#macos-desktop-app) · [MCP Setup](#connect-your-ai-agent) · [CLI Reference](#cli) · [Architecture](#architecture)
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ## Product Tour
25
+
26
+ <div align="center">
27
+
28
+ <a href="https://www.nomoreide.com/#hero-demo">
29
+ <img src="assets/screenshots/product-tour.png" alt="NoMoreIDE Git Review showing running services, changed and staged files, an inline diff, commit controls, and the agent terminal dock" width="1200" />
30
+ </a>
31
+
32
+ <sub>Review a repository, inspect a diff, stage and commit changes, watch running services, and hand context to an agent without leaving the workbench. <a href="https://www.nomoreide.com/#hero-demo">Try the interactive demo</a>—it uses safe mock data.</sub>
19
33
 
20
34
  </div>
21
35
 
@@ -23,29 +37,33 @@ Give your coding agents and yourself a **shared local control surface** for serv
23
37
 
24
38
  ## What Is NoMoreIDE?
25
39
 
26
- NoMoreIDE is a lightweight process manager, Git reviewer, GitHub workflow surface, database workbench, log aggregator, and MCP server — all in one `npx` command. It gives AI coding agents (Claude Code, Codex CLI, Gemini CLI, and others) a safe, structured window into your running dev environment through the **Model Context Protocol (MCP)**, while also providing a terminal UI and a local React web dashboard for humans.
40
+ NoMoreIDE is the shared local control plane between you and your coding agents. The native macOS app, web dashboard, TUI, CLI, and MCP server all operate on the same services, repositories, logs, databases, workflows, and agent configuration.
27
41
 
42
+ - **For humans:** one place to run services, inspect logs and ports, review Git changes, manage GitHub work, browse databases, and open terminals.
43
+ - **For agents:** structured MCP tools expose the same live project state to Claude Code, Codex CLI, Gemini CLI, and other MCP clients.
44
+ - **For safety:** destructive Git operations are omitted, database writes require human approval, secrets are redacted from shared profiles, and NoMoreIDE does not kill processes it did not start.
45
+
46
+ ---
47
+
48
+ ## Download or Run NoMoreIDE
49
+
50
+ ### macOS Desktop App
51
+
52
+ Prefer a native app? Download the latest universal DMG for both Apple silicon and Intel Macs:
53
+
54
+ **[Download NoMoreIDE for macOS](https://www.nomoreide.com/#download)** · [View all releases](https://github.com/Rorogogogo/nomoreide/releases)
55
+
56
+ > **First launch:** NoMoreIDE is currently unsigned and not notarized, so macOS will block it the first time you open it. After dragging NoMoreIDE to Applications, try to open it once, then go to **System Settings → Privacy & Security**, click **Open Anyway**, and confirm with Touch ID or your password.
57
+
58
+ ### CLI, Web UI, TUI, and MCP Server
59
+
60
+ No global installation is required. Run the latest version with Node.js 20 or newer:
61
+
62
+ ```bash
63
+ npx -y nomoreide
28
64
  ```
29
- ┌──────────────────────────────────────────────────────┐
30
- │ Your Project │
31
- │ │
32
- │ Claude Code / Codex CLI / Gemini CLI │
33
- │ │ │
34
- │ MCP (stdio) │
35
- │ │ │
36
- │ ┌───────▼────────┐ ┌──────────────────────┐ │
37
- │ │ NoMoreIDE MCP │◄──►│ Process Manager │ │
38
- │ │ Server │ │ Log Store │ │
39
- │ └───────┬────────┘ │ Git Manager │ │
40
- │ │ │ Config Store │ │
41
- │ HTTP API └──────────────────────┘ │
42
- │ │ │
43
- │ ┌───────▼──────────────────────────┐ │
44
- │ │ Web UI (localhost:4317) │ │
45
- │ │ Terminal UI (nomoreide tui) │ │
46
- │ └──────────────────────────────────┘ │
47
- └──────────────────────────────────────────────────────┘
48
- ```
65
+
66
+ Use the agent-specific commands below to connect NoMoreIDE as an MCP server.
49
67
 
50
68
  ---
51
69
 
@@ -168,6 +186,7 @@ graph TD
168
186
  GM[Git Manager]
169
187
  LS[Log Store]
170
188
  CS[Config Store<br/>nomoreide.config.json]
189
+ Desktop[macOS Desktop App]
171
190
  WS[Web Server<br/>:4317]
172
191
  TUI[Terminal UI]
173
192
  end
@@ -187,6 +206,9 @@ graph TD
187
206
  PM --> S2
188
207
  PM --> S3
189
208
  LS --> PM
209
+ Desktop --> PM
210
+ Desktop --> GM
211
+ Desktop --> LS
190
212
  WS --> PM
191
213
  WS --> GM
192
214
  WS --> LS
@@ -198,7 +220,7 @@ graph TD
198
220
 
199
221
  ## Feature Overview
200
222
 
201
- | Feature | CLI | TUI | Web UI | MCP |
223
+ | Feature | CLI | TUI | Desktop / Web | MCP |
202
224
  |---|:---:|:---:|:---:|:---:|
203
225
  | Start / stop / restart services | ✓ | ✓ | ✓ | ✓ |
204
226
  | Bundle orchestration | ✓ | | ✓ | ✓ |
@@ -3,10 +3,18 @@ export interface InteractiveAgentInvocation {
3
3
  shell: string;
4
4
  args: string[];
5
5
  }
6
+ export interface InteractiveAgentOptions {
7
+ /** Prior session to reopen, from `listAgentTranscripts`. */
8
+ resumeId?: string;
9
+ }
6
10
  /**
7
11
  * Both CLIs accept a positional initial prompt and queue it themselves until
8
12
  * the interactive TUI is ready. Passing it as an argument is what makes the
9
13
  * first prompt reliable — injecting keystrokes after spawn raced the TUI's
10
14
  * startup (trust dialogs, first paint) and the paste was silently dropped.
15
+ *
16
+ * A blank prompt starts the provider's interactive TUI without queueing a
17
+ * first turn. Resuming reopens a recorded session and replays its history, so
18
+ * an empty prompt there just returns to the conversation where it left off.
11
19
  */
12
- export declare function buildInteractiveAgentInvocation(provider: string, prompt: string): InteractiveAgentInvocation;
20
+ export declare function buildInteractiveAgentInvocation(provider: string, prompt: string, { resumeId }?: InteractiveAgentOptions): InteractiveAgentInvocation;
@@ -1,20 +1,41 @@
1
1
  const CLAUDE_BIN = process.env.NOMOREIDE_CLAUDE_BIN || "claude";
2
2
  const CODEX_BIN = process.env.NOMOREIDE_CODEX_BIN || "codex";
3
+ /**
4
+ * Session ids are spawned as argv entries rather than through a shell, so the
5
+ * risk is not injection but a malformed id starting with `-` and being read as
6
+ * a flag by the CLI. Both providers issue UUIDs, so anything else is refused.
7
+ */
8
+ const SESSION_ID = /^[0-9a-fA-F-]{8,64}$/;
3
9
  /**
4
10
  * Both CLIs accept a positional initial prompt and queue it themselves until
5
11
  * the interactive TUI is ready. Passing it as an argument is what makes the
6
12
  * first prompt reliable — injecting keystrokes after spawn raced the TUI's
7
13
  * startup (trust dialogs, first paint) and the paste was silently dropped.
14
+ *
15
+ * A blank prompt starts the provider's interactive TUI without queueing a
16
+ * first turn. Resuming reopens a recorded session and replays its history, so
17
+ * an empty prompt there just returns to the conversation where it left off.
8
18
  */
9
- export function buildInteractiveAgentInvocation(provider, prompt) {
10
- if (!prompt.trim()) {
11
- throw new Error("Prompt is required");
19
+ export function buildInteractiveAgentInvocation(provider, prompt, { resumeId } = {}) {
20
+ if (resumeId !== undefined && !SESSION_ID.test(resumeId)) {
21
+ throw new Error(`Invalid session id: ${String(resumeId)}`);
12
22
  }
23
+ // A resumed session carries its own history; an empty prompt must not be
24
+ // forwarded as an empty positional argument.
25
+ const trailing = prompt.trim() ? [prompt] : [];
13
26
  switch (provider) {
14
27
  case "claude":
15
- return { shell: CLAUDE_BIN, args: [prompt] };
28
+ return {
29
+ shell: CLAUDE_BIN,
30
+ args: resumeId ? ["--resume", resumeId, ...trailing] : trailing,
31
+ };
16
32
  case "codex":
17
- return { shell: CODEX_BIN, args: ["--no-alt-screen", prompt] };
33
+ return {
34
+ shell: CODEX_BIN,
35
+ args: resumeId
36
+ ? ["--no-alt-screen", "resume", resumeId, ...trailing]
37
+ : ["--no-alt-screen", ...trailing],
38
+ };
18
39
  default:
19
40
  throw new Error(`Unsupported agent provider: ${String(provider)}`);
20
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-terminal.js","sourceRoot":"","sources":["../../src/core/agent-terminal.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,QAAQ,CAAC;AAChE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC;AAS7D;;;;;GAKG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAAgB,EAChB,MAAc;IAEd,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACxC,CAAC;IAED,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/C,KAAK,OAAO;YACV,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,iBAAiB,EAAE,MAAM,CAAC,EAAE,CAAC;QACjE;YACE,MAAM,IAAI,KAAK,CAAC,+BAA+B,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"agent-terminal.js","sourceRoot":"","sources":["../../src/core/agent-terminal.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,QAAQ,CAAC;AAChE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC;AAc7D;;;;GAIG;AACH,MAAM,UAAU,GAAG,sBAAsB,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAAgB,EAChB,MAAc,EACd,EAAE,QAAQ,KAA8B,EAAE;IAE1C,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,yEAAyE;IACzE,6CAA6C;IAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE/C,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO;gBACL,KAAK,EAAE,UAAU;gBACjB,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ;aAChE,CAAC;QACJ,KAAK,OAAO;YACV,OAAO;gBACL,KAAK,EAAE,SAAS;gBAChB,IAAI,EAAE,QAAQ;oBACZ,CAAC,CAAC,CAAC,iBAAiB,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAAC;oBACtD,CAAC,CAAC,CAAC,iBAAiB,EAAE,GAAG,QAAQ,CAAC;aACrC,CAAC;QACJ;YACE,MAAM,IAAI,KAAK,CAAC,+BAA+B,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACvE,CAAC;AACH,CAAC"}
@@ -0,0 +1,27 @@
1
+ export type TranscriptProvider = "claude" | "codex";
2
+ export interface AgentTranscript {
3
+ /** Session id accepted by `claude --resume <id>` / `codex resume <id>`. */
4
+ id: string;
5
+ provider: TranscriptProvider;
6
+ /** Working directory the session ran in, read from the transcript body. */
7
+ cwd: string;
8
+ /** First real user prompt, used as the picker label. */
9
+ title: string;
10
+ startedAt: string;
11
+ /** File mtime — when the session was last written to. */
12
+ updatedAt: string;
13
+ }
14
+ export interface ListAgentTranscriptsOptions {
15
+ repoPath: string;
16
+ limit?: number;
17
+ /** Overridable for tests; defaults to the real home directory. */
18
+ homeDir?: string;
19
+ /** Overridable for tests and non-default Codex installations. */
20
+ codexHome?: string;
21
+ }
22
+ /**
23
+ * Prior Claude and Codex sessions recorded for `repoPath`, most recently
24
+ * written first. Metadata only — the transcript bodies are never surfaced,
25
+ * because resuming hands the conversation back to the CLI that rendered it.
26
+ */
27
+ export declare function listAgentTranscripts(options: ListAgentTranscriptsOptions): Promise<AgentTranscript[]>;
@@ -0,0 +1,240 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { readdir, stat } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ /**
7
+ * Both CLIs append newline-delimited JSON, so a session's identity, cwd and
8
+ * opening prompt all land in the first handful of lines. Transcripts run to
9
+ * megabytes, so every read stops as soon as those are in hand rather than
10
+ * parsing the whole conversation.
11
+ */
12
+ const MAX_HEAD_LINES = 500;
13
+ /** Upper bound on Codex rollout files opened per listing (they carry no path index). */
14
+ const MAX_CODEX_SCAN = 400;
15
+ /** Enough history for the dock picker to be useful without scanning unbounded metadata. */
16
+ const DEFAULT_TRANSCRIPT_LIMIT = 100;
17
+ const MAX_TITLE = 200;
18
+ /**
19
+ * Claude encodes the cwd into a directory name lossily, and the encoding has
20
+ * changed between releases — `~/.claude/projects` currently holds both
21
+ * `...-Personal Project-brainctl` and `...-Personal-Project-brainctl`, and both
22
+ * `...-extension-3.0` and `...-extension-3-0`. Collapsing every non-alphanumeric
23
+ * run to a single `-` makes those variants compare equal, so one repo's history
24
+ * is found across all the directories past versions wrote it to. The comparison
25
+ * is deliberately lossy in the other direction too (sibling paths can collide),
26
+ * which is why every candidate file is confirmed against the `cwd` recorded in
27
+ * its own body before being returned.
28
+ */
29
+ function pathKey(value) {
30
+ return value.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
31
+ }
32
+ /** Reads leading lines as JSON, stopping when `onEntry` reports it has enough. */
33
+ async function readHead(filePath, onEntry) {
34
+ const stream = createReadStream(filePath, { encoding: "utf8" });
35
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
36
+ let seen = 0;
37
+ try {
38
+ for await (const line of lines) {
39
+ if (++seen > MAX_HEAD_LINES)
40
+ break;
41
+ if (!line.trim())
42
+ continue;
43
+ let entry;
44
+ try {
45
+ entry = JSON.parse(line);
46
+ }
47
+ catch {
48
+ continue; // A truncated tail line must not discard the whole session.
49
+ }
50
+ if (entry && typeof entry === "object" && onEntry(entry))
51
+ break;
52
+ }
53
+ }
54
+ finally {
55
+ lines.close();
56
+ stream.destroy();
57
+ }
58
+ }
59
+ /**
60
+ * Injected context (environment blocks, slash-command expansions, system
61
+ * reminders) is recorded as a user turn by both CLIs, so anything opening with
62
+ * a tag is skipped in favour of the first prompt a human actually typed.
63
+ */
64
+ function isTypedPrompt(text) {
65
+ const trimmed = text.trim();
66
+ return trimmed.length > 0
67
+ && !trimmed.startsWith("<")
68
+ && !trimmed.startsWith("# AGENTS.md instructions for ");
69
+ }
70
+ function toTitle(text) {
71
+ const collapsed = text.trim().replace(/\s+/g, " ");
72
+ return collapsed.length > MAX_TITLE ? `${collapsed.slice(0, MAX_TITLE - 1).trimEnd()}…` : collapsed;
73
+ }
74
+ /** Claude message content is either a plain string or an array of typed blocks. */
75
+ function claudeText(content) {
76
+ if (typeof content === "string")
77
+ return content;
78
+ if (!Array.isArray(content))
79
+ return "";
80
+ return content
81
+ .map((block) => (block && typeof block === "object" && typeof block.text === "string" ? block.text : ""))
82
+ .join("");
83
+ }
84
+ function codexText(content) {
85
+ if (!Array.isArray(content))
86
+ return "";
87
+ return content
88
+ .map((block) => (block && typeof block === "object" && typeof block.text === "string" ? block.text : ""))
89
+ .join("");
90
+ }
91
+ async function readClaudeTranscript(filePath, fallbackId) {
92
+ let id;
93
+ let cwd;
94
+ let title;
95
+ let startedAt;
96
+ await readHead(filePath, (entry) => {
97
+ if (!id && typeof entry.sessionId === "string")
98
+ id = entry.sessionId;
99
+ if (!cwd && typeof entry.cwd === "string")
100
+ cwd = entry.cwd;
101
+ if (!startedAt && typeof entry.timestamp === "string")
102
+ startedAt = entry.timestamp;
103
+ if (!title && entry.type === "user" && entry.isSidechain !== true) {
104
+ const message = entry.message;
105
+ const text = claudeText(message && typeof message === "object" ? message.content : undefined);
106
+ if (isTypedPrompt(text))
107
+ title = toTitle(text);
108
+ }
109
+ return Boolean(cwd && title);
110
+ });
111
+ if (!cwd)
112
+ return null;
113
+ const { mtime } = await stat(filePath);
114
+ return {
115
+ id: id ?? fallbackId,
116
+ provider: "claude",
117
+ cwd,
118
+ title: title ?? "",
119
+ startedAt: startedAt ?? mtime.toISOString(),
120
+ updatedAt: mtime.toISOString(),
121
+ };
122
+ }
123
+ /**
124
+ * Codex records no per-project index, so a listing opens every recent rollout.
125
+ * `expectedCwd` lets a session that belongs to another project stop at its
126
+ * `session_meta` line instead of being read on in search of a prompt.
127
+ */
128
+ async function readCodexTranscript(filePath, expectedCwd) {
129
+ let id;
130
+ let cwd;
131
+ let title;
132
+ let startedAt;
133
+ await readHead(filePath, (entry) => {
134
+ const payload = entry.payload;
135
+ if (!payload || typeof payload !== "object")
136
+ return false;
137
+ const body = payload;
138
+ if (entry.type === "session_meta") {
139
+ if (typeof body.session_id === "string")
140
+ id = body.session_id;
141
+ else if (typeof body.id === "string")
142
+ id = body.id;
143
+ if (typeof body.cwd === "string")
144
+ cwd = body.cwd;
145
+ if (typeof body.timestamp === "string")
146
+ startedAt = body.timestamp;
147
+ return cwd !== expectedCwd;
148
+ }
149
+ if (!title && body.type === "message" && body.role === "user") {
150
+ const text = codexText(body.content);
151
+ if (isTypedPrompt(text))
152
+ title = toTitle(text);
153
+ }
154
+ return Boolean(title);
155
+ });
156
+ if (!id || cwd !== expectedCwd)
157
+ return null;
158
+ const { mtime } = await stat(filePath);
159
+ return {
160
+ id,
161
+ provider: "codex",
162
+ cwd,
163
+ title: title ?? "",
164
+ startedAt: startedAt ?? mtime.toISOString(),
165
+ updatedAt: mtime.toISOString(),
166
+ };
167
+ }
168
+ async function listDir(path) {
169
+ try {
170
+ return await readdir(path);
171
+ }
172
+ catch {
173
+ return []; // A provider that was never installed simply contributes nothing.
174
+ }
175
+ }
176
+ async function claudeTranscripts(homeDir, repoPath) {
177
+ const root = join(homeDir, ".claude", "projects");
178
+ const key = pathKey(repoPath);
179
+ const dirs = (await listDir(root)).filter((name) => pathKey(name) === key);
180
+ const results = [];
181
+ for (const dir of dirs) {
182
+ const files = (await listDir(join(root, dir))).filter((name) => name.endsWith(".jsonl"));
183
+ for (const file of files) {
184
+ const transcript = await readClaudeTranscript(join(root, dir, file), file.replace(/\.jsonl$/, ""));
185
+ if (transcript && transcript.cwd === repoPath)
186
+ results.push(transcript);
187
+ }
188
+ }
189
+ return results;
190
+ }
191
+ /** Codex buckets rollouts by `sessions/YYYY/MM/DD`, so newest dates are walked first. */
192
+ async function codexRolloutFiles(codexHome) {
193
+ const root = join(codexHome, "sessions");
194
+ const files = [];
195
+ const descending = (values) => values.sort((a, b) => b.localeCompare(a));
196
+ for (const year of descending(await listDir(root))) {
197
+ for (const month of descending(await listDir(join(root, year)))) {
198
+ for (const day of descending(await listDir(join(root, year, month)))) {
199
+ const dir = join(root, year, month, day);
200
+ for (const name of descending(await listDir(dir))) {
201
+ if (name.startsWith("rollout-") && name.endsWith(".jsonl"))
202
+ files.push(join(dir, name));
203
+ }
204
+ if (files.length >= MAX_CODEX_SCAN)
205
+ return files;
206
+ }
207
+ }
208
+ }
209
+ return files;
210
+ }
211
+ async function codexTranscripts(codexHome, repoPath) {
212
+ const results = [];
213
+ for (const filePath of await codexRolloutFiles(codexHome)) {
214
+ const transcript = await readCodexTranscript(filePath, repoPath);
215
+ if (transcript)
216
+ results.push(transcript);
217
+ }
218
+ return results;
219
+ }
220
+ /**
221
+ * Prior Claude and Codex sessions recorded for `repoPath`, most recently
222
+ * written first. Metadata only — the transcript bodies are never surfaced,
223
+ * because resuming hands the conversation back to the CLI that rendered it.
224
+ */
225
+ export async function listAgentTranscripts(options) {
226
+ const { repoPath, limit = DEFAULT_TRANSCRIPT_LIMIT } = options;
227
+ const homeDir = options.homeDir ?? homedir();
228
+ const codexHome = options.codexHome
229
+ ?? (options.homeDir ? join(homeDir, ".codex") : process.env.CODEX_HOME)
230
+ ?? join(homeDir, ".codex");
231
+ const [claude, codex] = await Promise.all([
232
+ claudeTranscripts(homeDir, repoPath),
233
+ codexTranscripts(codexHome, repoPath),
234
+ ]);
235
+ return [...claude, ...codex]
236
+ .filter((transcript) => transcript.title)
237
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
238
+ .slice(0, limit);
239
+ }
240
+ //# sourceMappingURL=agent-transcripts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-transcripts.js","sourceRoot":"","sources":["../../src/core/agent-transcripts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAiBhD;;;;;GAKG;AACH,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,wFAAwF;AACxF,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,2FAA2F;AAC3F,MAAM,wBAAwB,GAAG,GAAG,CAAC;AACrC,MAAM,SAAS,GAAG,GAAG,CAAC;AAEtB;;;;;;;;;;GAUG;AACH,SAAS,OAAO,CAAC,KAAa;IAC5B,OAAO,KAAK,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AAClF,CAAC;AAED,kFAAkF;AAClF,KAAK,UAAU,QAAQ,CAAC,QAAgB,EAAE,OAAoD;IAC5F,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC/B,IAAI,EAAE,IAAI,GAAG,cAAc;gBAAE,MAAM;YACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,SAAS;YAC3B,IAAI,KAAc,CAAC;YACnB,IAAI,CAAC;gBACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS,CAAC,4DAA4D;YACxE,CAAC;YACD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAgC,CAAC;gBAAE,MAAM;QAC7F,CAAC;IACH,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC;WACpB,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;WACxB,CAAC,OAAO,CAAC,UAAU,CAAC,+BAA+B,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACtG,CAAC;AAED,mFAAmF;AACnF,SAAS,UAAU,CAAC,OAAgB;IAClC,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAChD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IACvC,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,KAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SACtJ,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,OAAgB;IACjC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IACvC,OAAO,OAAO;SACX,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,KAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SACtJ,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,UAAkB;IACtE,IAAI,EAAsB,CAAC;IAC3B,IAAI,GAAuB,CAAC;IAC5B,IAAI,KAAyB,CAAC;IAC9B,IAAI,SAA6B,CAAC;IAElC,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QACjC,IAAI,CAAC,EAAE,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;YAAE,EAAE,GAAG,KAAK,CAAC,SAAS,CAAC;QACrE,IAAI,CAAC,GAAG,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ;YAAE,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;QAC3D,IAAI,CAAC,SAAS,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ;YAAE,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QACnF,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAClE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAE,OAAiC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YACzH,IAAI,aAAa,CAAC,IAAI,CAAC;gBAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,OAAO,CAAC,GAAG,IAAI,KAAK,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,OAAO;QACL,EAAE,EAAE,EAAE,IAAI,UAAU;QACpB,QAAQ,EAAE,QAAQ;QAClB,GAAG;QACH,KAAK,EAAE,KAAK,IAAI,EAAE;QAClB,SAAS,EAAE,SAAS,IAAI,KAAK,CAAC,WAAW,EAAE;QAC3C,SAAS,EAAE,KAAK,CAAC,WAAW,EAAE;KAC/B,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,mBAAmB,CAAC,QAAgB,EAAE,WAAmB;IACtE,IAAI,EAAsB,CAAC;IAC3B,IAAI,GAAuB,CAAC;IAC5B,IAAI,KAAyB,CAAC;IAC9B,IAAI,SAA6B,CAAC;IAElC,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QACjC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC1D,MAAM,IAAI,GAAG,OAAkC,CAAC;QAChD,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAClC,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;gBAAE,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC;iBACzD,IAAI,OAAO,IAAI,CAAC,EAAE,KAAK,QAAQ;gBAAE,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;YACnD,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ;gBAAE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;YACjD,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ;gBAAE,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;YACnE,OAAO,GAAG,KAAK,WAAW,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC9D,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrC,IAAI,aAAa,CAAC,IAAI,CAAC;gBAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,EAAE,IAAI,GAAG,KAAK,WAAW;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvC,OAAO;QACL,EAAE;QACF,QAAQ,EAAE,OAAO;QACjB,GAAG;QACH,KAAK,EAAE,KAAK,IAAI,EAAE;QAClB,SAAS,EAAE,SAAS,IAAI,KAAK,CAAC,WAAW,EAAE;QAC3C,SAAS,EAAE,KAAK,CAAC,WAAW,EAAE;KAC/B,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,IAAY;IACjC,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,kEAAkE;IAC/E,CAAC;AACH,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,OAAe,EAAE,QAAgB;IAChE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAC3E,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QACzF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,UAAU,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;YACnG,IAAI,UAAU,IAAI,UAAU,CAAC,GAAG,KAAK,QAAQ;gBAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yFAAyF;AACzF,KAAK,UAAU,iBAAiB,CAAC,SAAiB;IAChD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACzC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,CAAC,MAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACnD,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;gBACzC,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;oBAClD,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;wBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;gBAC1F,CAAC;gBACD,IAAI,KAAK,CAAC,MAAM,IAAI,cAAc;oBAAE,OAAO,KAAK,CAAC;YACnD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,SAAiB,EAAE,QAAgB;IACjE,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,KAAK,MAAM,QAAQ,IAAI,MAAM,iBAAiB,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1D,MAAM,UAAU,GAAG,MAAM,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACjE,IAAI,UAAU;YAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAWD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAoC;IAEpC,MAAM,EAAE,QAAQ,EAAE,KAAK,GAAG,wBAAwB,EAAE,GAAG,OAAO,CAAC;IAC/D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC;IAC7C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS;WAC9B,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;WACpE,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC7B,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACxC,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC;QACpC,gBAAgB,CAAC,SAAS,EAAE,QAAQ,CAAC;KACtC,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,CAAC;SACzB,MAAM,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;SACxC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;SACtD,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AACrB,CAAC"}
@@ -61,6 +61,15 @@ export declare class ConfigStore {
61
61
  private enqueueMutation;
62
62
  private mutateConfig;
63
63
  registerService(service: ServiceDefinition): Promise<NoMoreIdeConfig>;
64
+ /**
65
+ * Patches only the project assignment. Reassignment goes through here rather
66
+ * than `registerService` because that replaces the whole definition by name —
67
+ * a caller would have to resend every field, and any field it did not know
68
+ * about would be silently dropped.
69
+ *
70
+ * A blank or absent `projectPath` clears the assignment back to cwd inference.
71
+ */
72
+ setServiceProject(name: string, projectPath: string | undefined): Promise<NoMoreIdeConfig>;
64
73
  removeService(name: string): Promise<NoMoreIdeConfig>;
65
74
  registerBundle(bundle: BundleDefinition, previousName?: string): Promise<NoMoreIdeConfig>;
66
75
  registerGitRepository(repository: GitRepositoryDefinition): Promise<NoMoreIdeConfig>;
@@ -94,5 +103,14 @@ export declare class ConfigStore {
94
103
  export declare class ConfigValidationError extends Error {
95
104
  }
96
105
  export declare function isGitWorktree(path: string): Promise<boolean>;
106
+ /**
107
+ * The worktree root enclosing `path`, or null when it is not inside a repo.
108
+ *
109
+ * Registering a service's own `cwd` is usually the wrong granularity — a service
110
+ * often runs from a subdirectory (`repo/frontend`), and a project pinned there
111
+ * would fight the path-based grouping. Resolving to the root first is what makes
112
+ * "adopt this service's folder" produce the project the user actually meant.
113
+ */
114
+ export declare function gitToplevel(path: string): Promise<string | null>;
97
115
  /** @internal Deterministic stale-lock ownership seam used by regression tests. */
98
116
  export declare function __recoverStaleConfigLockForTests(lockPath: string, afterOwnerRead: () => void | Promise<void>): Promise<void>;
@@ -20,6 +20,13 @@ const baseServiceSchema = z.object({
20
20
  test: z.string().min(1).optional(),
21
21
  /** Services that must be running/healthy before this one (bundle start order). */
22
22
  dependsOn: z.array(z.string().min(1)).optional(),
23
+ /**
24
+ * Explicit project assignment, overriding the cwd-containment inference.
25
+ * Needed because a remote service's `cwd` is a path on another machine
26
+ * (`/var/www/...`, `/root`) and can never sit under a local repo root, so
27
+ * inference alone leaves those services in no project at all.
28
+ */
29
+ projectPath: z.string().min(1).optional(),
23
30
  });
24
31
  const localServiceSchema = baseServiceSchema.extend({
25
32
  kind: z.literal("local").optional(),
@@ -282,6 +289,33 @@ export class ConfigStore {
282
289
  ];
283
290
  });
284
291
  }
292
+ /**
293
+ * Patches only the project assignment. Reassignment goes through here rather
294
+ * than `registerService` because that replaces the whole definition by name —
295
+ * a caller would have to resend every field, and any field it did not know
296
+ * about would be silently dropped.
297
+ *
298
+ * A blank or absent `projectPath` clears the assignment back to cwd inference.
299
+ */
300
+ async setServiceProject(name, projectPath) {
301
+ const serviceName = name.trim();
302
+ if (!serviceName) {
303
+ throw new ConfigValidationError("service name is required");
304
+ }
305
+ const assigned = projectPath?.trim();
306
+ return this.mutateConfig((config) => {
307
+ const service = config.services.find((item) => item.name === serviceName);
308
+ if (!service) {
309
+ throw new Error(`Service "${serviceName}" is not registered.`);
310
+ }
311
+ if (assigned) {
312
+ service.projectPath = assigned;
313
+ }
314
+ else {
315
+ delete service.projectPath;
316
+ }
317
+ });
318
+ }
285
319
  async removeService(name) {
286
320
  const serviceName = name.trim();
287
321
  if (!serviceName) {
@@ -472,6 +506,25 @@ export async function isGitWorktree(path) {
472
506
  return false;
473
507
  }
474
508
  }
509
+ /**
510
+ * The worktree root enclosing `path`, or null when it is not inside a repo.
511
+ *
512
+ * Registering a service's own `cwd` is usually the wrong granularity — a service
513
+ * often runs from a subdirectory (`repo/frontend`), and a project pinned there
514
+ * would fight the path-based grouping. Resolving to the root first is what makes
515
+ * "adopt this service's folder" produce the project the user actually meant.
516
+ */
517
+ export async function gitToplevel(path) {
518
+ try {
519
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
520
+ cwd: path,
521
+ });
522
+ return stdout.trim() || null;
523
+ }
524
+ catch {
525
+ return null;
526
+ }
527
+ }
475
528
  async function requireGitWorktree(path) {
476
529
  if (await isGitWorktree(path))
477
530
  return;