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.
- package/README.md +46 -24
- package/dist/core/agent-terminal.d.ts +9 -1
- package/dist/core/agent-terminal.js +26 -5
- package/dist/core/agent-terminal.js.map +1 -1
- package/dist/core/agent-transcripts.d.ts +27 -0
- package/dist/core/agent-transcripts.js +240 -0
- package/dist/core/agent-transcripts.js.map +1 -0
- package/dist/core/config-store.d.ts +18 -0
- package/dist/core/config-store.js +53 -0
- package/dist/core/config-store.js.map +1 -1
- package/dist/core/docker-inspect.d.ts +34 -0
- package/dist/core/docker-inspect.js +108 -0
- package/dist/core/docker-inspect.js.map +1 -0
- package/dist/core/docker-resources.d.ts +28 -0
- package/dist/core/docker-resources.js +59 -0
- package/dist/core/docker-resources.js.map +1 -0
- package/dist/core/docker-stats.d.ts +22 -0
- package/dist/core/docker-stats.js +52 -0
- package/dist/core/docker-stats.js.map +1 -0
- package/dist/core/docker.d.ts +15 -0
- package/dist/core/docker.js +38 -1
- package/dist/core/docker.js.map +1 -1
- package/dist/core/repo-create.d.ts +11 -0
- package/dist/core/repo-create.js +54 -0
- package/dist/core/repo-create.js.map +1 -0
- package/dist/core/terminal-manager.d.ts +2 -0
- package/dist/core/terminal-manager.js +6 -0
- package/dist/core/terminal-manager.js.map +1 -1
- package/dist/core/terminal-session.d.ts +9 -1
- package/dist/core/terminal-session.js +24 -1
- package/dist/core/terminal-session.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/web/client/assets/SymbolsNerdFontMono-Regular-DDncdh2F.ttf +0 -0
- package/dist/web/client/assets/{code-editor-Cw2LhseL.js → code-editor-CRI31ZMh.js} +1 -1
- package/dist/web/client/assets/index-CQU15q2M.css +1 -0
- package/dist/web/client/assets/index-hu3LqMhU.js +228 -0
- package/dist/web/client/index.html +2 -2
- package/dist/web/routes/docker-routes.js +29 -2
- package/dist/web/routes/docker-routes.js.map +1 -1
- package/dist/web/routes/git-routes.js +36 -0
- package/dist/web/routes/git-routes.js.map +1 -1
- package/dist/web/routes/service-routes.js +21 -0
- package/dist/web/routes/service-routes.js.map +1 -1
- package/dist/web/routes/terminal-routes.js +37 -7
- package/dist/web/routes/terminal-routes.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/client/assets/index-D4SJDKDx.js +0 -228
- 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.
|
|
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) · [
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
11
|
-
throw new Error(
|
|
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 {
|
|
28
|
+
return {
|
|
29
|
+
shell: CLAUDE_BIN,
|
|
30
|
+
args: resumeId ? ["--resume", resumeId, ...trailing] : trailing,
|
|
31
|
+
};
|
|
16
32
|
case "codex":
|
|
17
|
-
return {
|
|
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;
|
|
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":"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"}
|
|
@@ -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;
|