inter-agent-bridge-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/PROTOCOL.md +65 -0
- package/README.md +183 -0
- package/fixtures/handoff-report.json +32 -0
- package/fixtures/session-store/claude/projects/sample/session-claude-fixture-0001.jsonl +2 -0
- package/fixtures/session-store/codex/sessions/2026/01/01/session-codex-fixture-0001.jsonl +2 -0
- package/fixtures/session-store/gemini/tmp/demo/chats/session-gemini-fixture-0001.json +7 -0
- package/package.json +31 -0
- package/schemas/handoff.schema.json +64 -0
- package/schemas/read-output.schema.json +25 -0
- package/schemas/report.schema.json +60 -0
- package/scripts/read_session.cjs +794 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Inter-Agent Bridge contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PROTOCOL.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Inter-Agent Bridge Protocol v0.2.0
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Define a lightweight, local-first standard for reading and coordinating cross-agent session evidence across Codex, Gemini, and Claude.
|
|
5
|
+
|
|
6
|
+
## Tenets
|
|
7
|
+
1. Local-first: read local session logs only by default.
|
|
8
|
+
2. Evidence-based: every claim must map to source sessions.
|
|
9
|
+
3. Context-light: return concise structured output first.
|
|
10
|
+
4. Dual implementation parity: Node and Rust must follow the same command and JSON contract.
|
|
11
|
+
|
|
12
|
+
## Canonical Modes
|
|
13
|
+
- `verify`
|
|
14
|
+
- `steer`
|
|
15
|
+
- `analyze`
|
|
16
|
+
- `feedback`
|
|
17
|
+
|
|
18
|
+
## CLI Contract (v0.2)
|
|
19
|
+
Both implementations must support:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bridge read --agent <codex|gemini|claude> [--id=<substring>] [--cwd=<path>] [--chats-dir=<path>] [--json]
|
|
23
|
+
bridge compare --source <agent[:session-substring]>... [--cwd=<path>] [--json]
|
|
24
|
+
bridge report --handoff <path-to-handoff.json> [--cwd=<path>] [--json]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Rules:
|
|
28
|
+
1. `--cwd` defaults to current working directory when not provided.
|
|
29
|
+
2. If `--id` is provided, select the most recently modified session file whose path contains the substring.
|
|
30
|
+
3. If `--id` is not provided, select newest session scoped by cwd when possible.
|
|
31
|
+
4. If cwd-scoped session is missing for Codex/Claude, warn and fall back to latest global session.
|
|
32
|
+
5. Hard failures must exit non-zero and print a concise error.
|
|
33
|
+
|
|
34
|
+
## JSON Output Contract (`bridge read --json`)
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"agent": "codex",
|
|
39
|
+
"source": "/absolute/path/to/session-file",
|
|
40
|
+
"content": "last assistant/model turn or fallback text",
|
|
41
|
+
"warnings": [
|
|
42
|
+
"Warning: no Codex session matched cwd /path; falling back to latest session."
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Schema is defined in `schemas/read-output.schema.json`.
|
|
48
|
+
|
|
49
|
+
`bridge report --json` outputs the coordinator report object defined by `schemas/report.schema.json`.
|
|
50
|
+
`bridge report --handoff` consumes packets defined by `schemas/handoff.schema.json`.
|
|
51
|
+
|
|
52
|
+
## Redaction Rules
|
|
53
|
+
Implementations must redact likely secrets from returned content before printing:
|
|
54
|
+
- `sk-...` style API keys
|
|
55
|
+
- `AKIA...` style AWS access key IDs
|
|
56
|
+
- `Bearer <token>` headers
|
|
57
|
+
- `api_key|token|secret|password` key-value pairs
|
|
58
|
+
|
|
59
|
+
## Environment Overrides (for testing and controlled installs)
|
|
60
|
+
- `BRIDGE_CODEX_SESSIONS_DIR`
|
|
61
|
+
- `BRIDGE_GEMINI_TMP_DIR`
|
|
62
|
+
- `BRIDGE_CLAUDE_PROJECTS_DIR`
|
|
63
|
+
|
|
64
|
+
## Conformance
|
|
65
|
+
Any release must pass `scripts/conformance.sh`, which runs both implementations against shared fixtures and verifies equivalent JSON output.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Inter-Agent Bridge
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**Inter-Agent Bridge** is a lightweight local protocol and reference implementation for reading cross-agent session context. It enables AI agents (Codex, Gemini, Claude) to read each other's recent session outputs from local storage, facilitating coordination, verification, and steering without a centralized cloud service.
|
|
8
|
+
|
|
9
|
+
## 🌟 Key Tenets
|
|
10
|
+
|
|
11
|
+
1. **Local-First**: Reads directly from local session logs (`~/.codex/sessions`, etc.) by default and does not call external services for `read`, `compare`, or `report`.
|
|
12
|
+
2. **Evidence-Based**: Every claim or summary must track back to a specific source session file.
|
|
13
|
+
3. **Privacy-Focused**: Automatically redacts sensitive keys (API keys, AWS tokens) before output.
|
|
14
|
+
4. **Dual Parity**: Ships with both **Node.js** and **Rust** CLIs that are conformance-tested against the same output contract.
|
|
15
|
+
|
|
16
|
+
## 🎥 Demo
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
## 🏗️ Architecture
|
|
21
|
+
|
|
22
|
+
The bridge acts as a universal translator for agent session formats.
|
|
23
|
+
|
|
24
|
+
```mermaid
|
|
25
|
+
sequenceDiagram
|
|
26
|
+
participant User
|
|
27
|
+
participant BridgeCLI
|
|
28
|
+
participant Codex as ~/.codex/sessions
|
|
29
|
+
participant Gemini as ~/.gemini/tmp
|
|
30
|
+
participant Claude as ~/.claude/projects
|
|
31
|
+
|
|
32
|
+
User->>BridgeCLI: bridge read --agent codex --id "fix-bug"
|
|
33
|
+
BridgeCLI->>Codex: Scan & Parse JSONL
|
|
34
|
+
Codex-->>BridgeCLI: Raw Session Data
|
|
35
|
+
BridgeCLI->>BridgeCLI: Redact Secrets (sk-..., AKIA...)
|
|
36
|
+
BridgeCLI->>BridgeCLI: Format via Schema
|
|
37
|
+
BridgeCLI-->>User: Structured JSON Output
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 🚀 Feature Matrix
|
|
41
|
+
|
|
42
|
+
| Feature | Codex | Gemini | Claude |
|
|
43
|
+
| :----------------- | :---: | :----: | :----: |
|
|
44
|
+
| **Read Content** | ✅ | ✅ | ✅ |
|
|
45
|
+
| **Auto-Discovery** | ✅ | ✅ | ✅ |
|
|
46
|
+
| **CWD Scoping** | ✅ | ⚠️ | ✅ |
|
|
47
|
+
| **Comparisons** | ✅ | ✅ | ✅ |
|
|
48
|
+
|
|
49
|
+
> ⚠️ Gemini resolves sessions by hashing the working directory path (SHA256) to locate chat files, rather than extracting CWD metadata from session content like Codex and Claude.
|
|
50
|
+
|
|
51
|
+
## 📦 Installation
|
|
52
|
+
|
|
53
|
+
### Consumers (After Release)
|
|
54
|
+
|
|
55
|
+
> Available after v0.2.0 is published to npm / crates.io.
|
|
56
|
+
|
|
57
|
+
**Node.js**:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install -g inter-agent-bridge-cli
|
|
61
|
+
bridge-node read --agent=codex --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Rust**:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cargo install bridge-cli
|
|
68
|
+
bridge read --agent codex --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Contributors (Developers)
|
|
72
|
+
|
|
73
|
+
Clone the repository to build from source.
|
|
74
|
+
|
|
75
|
+
**Node**:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install
|
|
79
|
+
node scripts/read_session.cjs read --agent=codex
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Rust**:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cargo run --manifest-path cli/Cargo.toml -- read --agent codex
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 📖 Usage
|
|
89
|
+
|
|
90
|
+
> **Note**: The examples below use the `bridge` command. If you installed via Node.js (`npm`), use `bridge-node` instead.
|
|
91
|
+
|
|
92
|
+
### Reading a Session
|
|
93
|
+
|
|
94
|
+
Get the last assistant/model output from a specific agent context.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Read from Codex (defaults to latest session)
|
|
98
|
+
bridge read --agent codex
|
|
99
|
+
|
|
100
|
+
# Read from Claude, scoped to current working directory
|
|
101
|
+
bridge read --agent claude --cwd /path/to/project
|
|
102
|
+
|
|
103
|
+
# Get machine-readable JSON output
|
|
104
|
+
bridge read --agent gemini --json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Comparing Agents (`analyze` mode)
|
|
108
|
+
|
|
109
|
+
Compare outputs from multiple agents to detect divergence.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bridge compare --source codex --source gemini --source claude --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Reporting
|
|
116
|
+
|
|
117
|
+
Generate a full coordination report from a handoff packet.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bridge report --handoff ./handoff_packet.json --json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Protocol-Accurate Command Contract
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bridge read --agent <codex|gemini|claude> [--id=<substring>] [--cwd=<path>] [--chats-dir=<path>] [--json]
|
|
127
|
+
bridge compare --source <agent[:session-substring]>... [--cwd=<path>] [--json]
|
|
128
|
+
bridge report --handoff <handoff.json> [--cwd=<path>] [--json]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- `read` returns the latest assistant/model output found in the selected session (or fallback raw lines when structured extraction fails).
|
|
132
|
+
- `compare` parses each `--source` as `<agent>` (current session) or `<agent>:<session-substring>`.
|
|
133
|
+
- `report` consumes a handoff packet and emits structured findings/recommendations.
|
|
134
|
+
|
|
135
|
+
## ⚙️ Configuration
|
|
136
|
+
|
|
137
|
+
Override default paths using environment variables.
|
|
138
|
+
|
|
139
|
+
| Variable | Description | Default |
|
|
140
|
+
| :--------------------------- | :------------------------ | :------------------- |
|
|
141
|
+
| `BRIDGE_CODEX_SESSIONS_DIR` | Path to Codex sessions | `~/.codex/sessions` |
|
|
142
|
+
| `BRIDGE_GEMINI_TMP_DIR` | Path to Gemini temp chats | `~/.gemini/tmp` |
|
|
143
|
+
| `BRIDGE_CLAUDE_PROJECTS_DIR` | Path to Claude projects | `~/.claude/projects` |
|
|
144
|
+
|
|
145
|
+
## 🛠️ Development
|
|
146
|
+
|
|
147
|
+
- **Protocol**: See [PROTOCOL.md](./PROTOCOL.md) for the CLI and JSON specification.
|
|
148
|
+
- **Skills**: See [SKILL.md](./SKILL.md) for agentic capabilities.
|
|
149
|
+
- **Release**: See [docs/release.md](./docs/release.md) for publishing workflows.
|
|
150
|
+
|
|
151
|
+
### Conformance Testing
|
|
152
|
+
|
|
153
|
+
Ensure both Node and Rust implementations return identical output for the same fixtures.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bash scripts/conformance.sh
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### README Command Checks
|
|
160
|
+
|
|
161
|
+
Run fixture-backed checks for command examples documented in this README.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
bash scripts/check_readme_examples.sh
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Schema Validation
|
|
168
|
+
|
|
169
|
+
Validate that generated reports match the JSON schema.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
bash scripts/validate_schemas.sh
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
If your environment is offline, use:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
BRIDGE_SKIP_AJV=1 bash scripts/validate_schemas.sh
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
_Maintained by the Agent Bridge Team._
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mode": "verify",
|
|
3
|
+
"task": "Validate fixture implementation parity",
|
|
4
|
+
"success_criteria": [
|
|
5
|
+
"All sources readable",
|
|
6
|
+
"No divergence in fixture outputs"
|
|
7
|
+
],
|
|
8
|
+
"sources": [
|
|
9
|
+
{
|
|
10
|
+
"agent": "codex",
|
|
11
|
+
"session_id": "codex-fixture",
|
|
12
|
+
"current_session": false,
|
|
13
|
+
"cwd": "/workspace/demo"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"agent": "gemini",
|
|
17
|
+
"session_id": "gemini-fixture",
|
|
18
|
+
"current_session": false,
|
|
19
|
+
"cwd": "/workspace/demo"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"agent": "claude",
|
|
23
|
+
"session_id": "claude-fixture",
|
|
24
|
+
"current_session": false,
|
|
25
|
+
"cwd": "/workspace/demo"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"constraints": [
|
|
29
|
+
"No cloud dependencies",
|
|
30
|
+
"Keep output concise"
|
|
31
|
+
]
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inter-agent-bridge-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/cote-star/agent-bridge.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/cote-star/agent-bridge",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cote-star/agent-bridge/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"bridge-node": "scripts/read_session.cjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"scripts/read_session.cjs",
|
|
20
|
+
"PROTOCOL.md",
|
|
21
|
+
"README.md",
|
|
22
|
+
"schemas",
|
|
23
|
+
"fixtures"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"conformance": "bash scripts/conformance.sh",
|
|
27
|
+
"check:readme": "bash scripts/check_readme_examples.sh",
|
|
28
|
+
"validate:schemas": "bash scripts/validate_schemas.sh",
|
|
29
|
+
"check": "npm run conformance && npm run check:readme && npm run validate:schemas"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://inter-agent-bridge.dev/schemas/handoff.schema.json",
|
|
4
|
+
"title": "Coordinator Handoff Packet",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["mode", "task", "success_criteria", "sources"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"mode": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["verify", "steer", "analyze", "feedback"]
|
|
12
|
+
},
|
|
13
|
+
"task": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1
|
|
16
|
+
},
|
|
17
|
+
"success_criteria": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string" },
|
|
20
|
+
"minItems": 1
|
|
21
|
+
},
|
|
22
|
+
"sources": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"required": ["agent"],
|
|
29
|
+
"anyOf": [
|
|
30
|
+
{
|
|
31
|
+
"required": ["session_id"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"session_id": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"minLength": 1
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"required": ["current_session"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"current_session": {
|
|
43
|
+
"const": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"properties": {
|
|
49
|
+
"agent": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"enum": ["codex", "gemini", "claude"]
|
|
52
|
+
},
|
|
53
|
+
"session_id": { "type": ["string", "null"] },
|
|
54
|
+
"current_session": { "type": "boolean" },
|
|
55
|
+
"cwd": { "type": "string" }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"constraints": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": { "type": "string" }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://inter-agent-bridge.dev/schemas/read-output.schema.json",
|
|
4
|
+
"title": "Bridge Read Output",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["agent", "source", "content", "warnings"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"agent": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["codex", "gemini", "claude"]
|
|
12
|
+
},
|
|
13
|
+
"source": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"minLength": 1
|
|
16
|
+
},
|
|
17
|
+
"content": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"warnings": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": { "type": "string" }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://inter-agent-bridge.dev/schemas/report.schema.json",
|
|
4
|
+
"title": "Coordinator Report",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["mode", "task", "sources_used", "verdict", "findings", "recommended_next_actions"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"mode": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["verify", "steer", "analyze", "feedback"]
|
|
12
|
+
},
|
|
13
|
+
"task": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"success_criteria": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": { "type": "string" }
|
|
19
|
+
},
|
|
20
|
+
"sources_used": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": { "type": "string" }
|
|
23
|
+
},
|
|
24
|
+
"verdict": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"findings": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["severity", "summary", "evidence", "confidence"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"severity": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["P0", "P1", "P2", "P3"]
|
|
37
|
+
},
|
|
38
|
+
"summary": { "type": "string" },
|
|
39
|
+
"evidence": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "type": "string" }
|
|
42
|
+
},
|
|
43
|
+
"confidence": {
|
|
44
|
+
"type": "number",
|
|
45
|
+
"minimum": 0,
|
|
46
|
+
"maximum": 1
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"recommended_next_actions": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": { "type": "string" }
|
|
54
|
+
},
|
|
55
|
+
"open_questions": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": { "type": "string" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const rawArgs = process.argv.slice(2);
|
|
9
|
+
const commandNames = new Set(['read', 'compare', 'report']);
|
|
10
|
+
const command = commandNames.has(rawArgs[0]) ? rawArgs[0] : 'read';
|
|
11
|
+
const args = commandNames.has(rawArgs[0]) ? rawArgs.slice(1) : rawArgs;
|
|
12
|
+
|
|
13
|
+
const codexSessionsBase = normalizePath(process.env.BRIDGE_CODEX_SESSIONS_DIR || '~/.codex/sessions');
|
|
14
|
+
const claudeProjectsBase = normalizePath(process.env.BRIDGE_CLAUDE_PROJECTS_DIR || '~/.claude/projects');
|
|
15
|
+
const geminiTmpBase = normalizePath(process.env.BRIDGE_GEMINI_TMP_DIR || '~/.gemini/tmp');
|
|
16
|
+
|
|
17
|
+
function expandHome(filepath) {
|
|
18
|
+
if (!filepath) return filepath;
|
|
19
|
+
if (filepath === '~') return os.homedir();
|
|
20
|
+
if (filepath.startsWith('~/')) {
|
|
21
|
+
return path.join(os.homedir(), filepath.slice(2));
|
|
22
|
+
}
|
|
23
|
+
return filepath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizePath(filepath) {
|
|
27
|
+
return path.resolve(expandHome(filepath));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hashPath(filepath) {
|
|
31
|
+
return crypto.createHash('sha256').update(normalizePath(filepath)).digest('hex');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getOptionValues(inputArgs, name) {
|
|
35
|
+
const values = [];
|
|
36
|
+
for (let i = 0; i < inputArgs.length; i += 1) {
|
|
37
|
+
const arg = inputArgs[i];
|
|
38
|
+
if (arg === name && i + 1 < inputArgs.length) {
|
|
39
|
+
values.push(inputArgs[i + 1]);
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const prefix = `${name}=`;
|
|
45
|
+
if (arg.startsWith(prefix)) {
|
|
46
|
+
values.push(arg.slice(prefix.length));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return values;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getOptionValue(inputArgs, name, fallback = null) {
|
|
53
|
+
const values = getOptionValues(inputArgs, name);
|
|
54
|
+
return values.length > 0 ? values[values.length - 1] : fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasFlag(inputArgs, name) {
|
|
58
|
+
return inputArgs.includes(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectMatchingFiles(dirPath, predicate, recursive = false) {
|
|
62
|
+
if (!dirPath || !fs.existsSync(dirPath)) return [];
|
|
63
|
+
|
|
64
|
+
const matches = [];
|
|
65
|
+
|
|
66
|
+
function search(currentDir) {
|
|
67
|
+
let entries = [];
|
|
68
|
+
try {
|
|
69
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
if (recursive) search(fullPath);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!predicate(fullPath, entry.name)) continue;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs.statSync(fullPath);
|
|
85
|
+
matches.push({ path: fullPath, mtimeMs: stat.mtimeMs });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Ignore entries that disappear while scanning.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
search(dirPath);
|
|
93
|
+
matches.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
94
|
+
return matches;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readJsonlLines(filePath) {
|
|
98
|
+
return fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findLatestByCwd(files, cwdExtractor, expectedCwd) {
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const fileCwd = cwdExtractor(file.path);
|
|
104
|
+
if (fileCwd && fileCwd === expectedCwd) {
|
|
105
|
+
return file.path;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getCodexSessionCwd(filePath) {
|
|
112
|
+
try {
|
|
113
|
+
const firstLine = readJsonlLines(filePath)[0];
|
|
114
|
+
if (!firstLine) return null;
|
|
115
|
+
|
|
116
|
+
const json = JSON.parse(firstLine);
|
|
117
|
+
if (json.type === 'session_meta' && json.payload && typeof json.payload.cwd === 'string') {
|
|
118
|
+
return normalizePath(json.payload.cwd);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getClaudeSessionCwd(filePath) {
|
|
127
|
+
try {
|
|
128
|
+
const lines = readJsonlLines(filePath);
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
try {
|
|
131
|
+
const json = JSON.parse(line);
|
|
132
|
+
if (typeof json.cwd === 'string') {
|
|
133
|
+
return normalizePath(json.cwd);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Ignore unparseable line.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function listGeminiChatDirs() {
|
|
146
|
+
if (!fs.existsSync(geminiTmpBase)) return [];
|
|
147
|
+
|
|
148
|
+
let entries = [];
|
|
149
|
+
try {
|
|
150
|
+
entries = fs.readdirSync(geminiTmpBase, { withFileTypes: true });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const dirs = [];
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (!entry.isDirectory()) continue;
|
|
158
|
+
const chatsDir = path.join(geminiTmpBase, entry.name, 'chats');
|
|
159
|
+
if (fs.existsSync(chatsDir)) {
|
|
160
|
+
dirs.push(chatsDir);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return dirs;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveGeminiChatDirs(chatsDir, cwd) {
|
|
167
|
+
if (chatsDir) {
|
|
168
|
+
const expanded = normalizePath(chatsDir);
|
|
169
|
+
return fs.existsSync(expanded) ? [expanded] : [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ordered = [];
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
|
|
175
|
+
function addDir(dirPath) {
|
|
176
|
+
if (!dirPath || seen.has(dirPath) || !fs.existsSync(dirPath)) return;
|
|
177
|
+
ordered.push(dirPath);
|
|
178
|
+
seen.add(dirPath);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const scopedHash = hashPath(cwd);
|
|
182
|
+
addDir(path.join(geminiTmpBase, scopedHash, 'chats'));
|
|
183
|
+
|
|
184
|
+
for (const dir of listGeminiChatDirs()) {
|
|
185
|
+
addDir(dir);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return ordered;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveCodexTargetFile(id, cwd, warnings) {
|
|
192
|
+
if (!fs.existsSync(codexSessionsBase)) return null;
|
|
193
|
+
|
|
194
|
+
if (id) {
|
|
195
|
+
const files = collectMatchingFiles(
|
|
196
|
+
codexSessionsBase,
|
|
197
|
+
(fullPath, name) => name.endsWith('.jsonl') && fullPath.includes(id),
|
|
198
|
+
true
|
|
199
|
+
);
|
|
200
|
+
return files.length > 0 ? files[0].path : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const files = collectMatchingFiles(codexSessionsBase, (fullPath, name) => name.endsWith('.jsonl'), true);
|
|
204
|
+
if (files.length === 0) return null;
|
|
205
|
+
|
|
206
|
+
const scoped = findLatestByCwd(files, getCodexSessionCwd, cwd);
|
|
207
|
+
if (scoped) return scoped;
|
|
208
|
+
|
|
209
|
+
warnings.push(`Warning: no Codex session matched cwd ${cwd}; falling back to latest session.`);
|
|
210
|
+
return files[0].path;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveClaudeTargetFile(id, cwd, warnings) {
|
|
214
|
+
if (!fs.existsSync(claudeProjectsBase)) return null;
|
|
215
|
+
|
|
216
|
+
if (id) {
|
|
217
|
+
const files = collectMatchingFiles(
|
|
218
|
+
claudeProjectsBase,
|
|
219
|
+
(fullPath, name) => name.endsWith('.jsonl') && fullPath.includes(id),
|
|
220
|
+
true
|
|
221
|
+
);
|
|
222
|
+
return files.length > 0 ? files[0].path : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const files = collectMatchingFiles(claudeProjectsBase, (fullPath, name) => name.endsWith('.jsonl'), true);
|
|
226
|
+
if (files.length === 0) return null;
|
|
227
|
+
|
|
228
|
+
const scoped = findLatestByCwd(files, getClaudeSessionCwd, cwd);
|
|
229
|
+
if (scoped) return scoped;
|
|
230
|
+
|
|
231
|
+
warnings.push(`Warning: no Claude session matched cwd ${cwd}; falling back to latest session.`);
|
|
232
|
+
return files[0].path;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveGeminiTargetFile(id, chatsDir, cwd) {
|
|
236
|
+
const dirs = resolveGeminiChatDirs(chatsDir, cwd);
|
|
237
|
+
if (dirs.length === 0) return { targetFile: null, searchedDirs: [] };
|
|
238
|
+
|
|
239
|
+
const candidates = [];
|
|
240
|
+
for (const dir of dirs) {
|
|
241
|
+
const files = collectMatchingFiles(
|
|
242
|
+
dir,
|
|
243
|
+
(fullPath, name) => {
|
|
244
|
+
if (!name.endsWith('.json')) return false;
|
|
245
|
+
if (id) return fullPath.includes(id);
|
|
246
|
+
return name.startsWith('session-');
|
|
247
|
+
},
|
|
248
|
+
false
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
for (const file of files) {
|
|
252
|
+
candidates.push(file);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
257
|
+
return {
|
|
258
|
+
targetFile: candidates.length > 0 ? candidates[0].path : null,
|
|
259
|
+
searchedDirs: dirs,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractText(value) {
|
|
264
|
+
if (typeof value === 'string') return value;
|
|
265
|
+
if (!Array.isArray(value)) return '';
|
|
266
|
+
|
|
267
|
+
return value
|
|
268
|
+
.map(part => {
|
|
269
|
+
if (typeof part === 'string') return part;
|
|
270
|
+
if (part && typeof part.text === 'string') return part.text;
|
|
271
|
+
return '';
|
|
272
|
+
})
|
|
273
|
+
.join('');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function extractClaudeText(value) {
|
|
277
|
+
if (typeof value === 'string') return value;
|
|
278
|
+
if (!Array.isArray(value)) return '';
|
|
279
|
+
|
|
280
|
+
return value
|
|
281
|
+
.filter(part => part && part.type === 'text')
|
|
282
|
+
.map(part => part.text || '')
|
|
283
|
+
.join('');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function redactSensitiveText(input) {
|
|
287
|
+
let output = String(input || '');
|
|
288
|
+
output = output.replace(/\bsk-[A-Za-z0-9]{20,}\b/g, 'sk-[REDACTED]');
|
|
289
|
+
output = output.replace(/\bAKIA[0-9A-Z]{16}\b/g, 'AKIA[REDACTED]');
|
|
290
|
+
output = output.replace(/\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi, 'Bearer [REDACTED]');
|
|
291
|
+
output = output.replace(
|
|
292
|
+
/\b(api[_-]?key|token|secret|password)\b\s*[:=]\s*["']?[^"'\s]+["']?/gi,
|
|
293
|
+
(_, key) => `${key}=[REDACTED]`
|
|
294
|
+
);
|
|
295
|
+
return output;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function readCodexSession(id, cwd) {
|
|
299
|
+
const warnings = [];
|
|
300
|
+
const targetFile = resolveCodexTargetFile(id, cwd, warnings);
|
|
301
|
+
if (!targetFile) {
|
|
302
|
+
throw new Error('No Codex session found.');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const lines = readJsonlLines(targetFile);
|
|
306
|
+
const messages = [];
|
|
307
|
+
let skipped = 0;
|
|
308
|
+
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
try {
|
|
311
|
+
const json = JSON.parse(line);
|
|
312
|
+
if (json.type === 'response_item' && json.payload && json.payload.type === 'message') {
|
|
313
|
+
messages.push(json.payload);
|
|
314
|
+
} else if (json.type === 'event_msg' && json.payload && json.payload.type === 'agent_message') {
|
|
315
|
+
messages.push({ role: 'assistant', content: json.payload.message });
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
skipped += 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (skipped > 0) {
|
|
323
|
+
warnings.push(`Warning: skipped ${skipped} unparseable line(s) in ${targetFile}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let content = '';
|
|
327
|
+
if (messages.length > 0) {
|
|
328
|
+
const assistantMsgs = messages.filter(message => (message.role || '').toLowerCase() === 'assistant');
|
|
329
|
+
const selected = assistantMsgs.length > 0 ? assistantMsgs[assistantMsgs.length - 1] : messages[messages.length - 1];
|
|
330
|
+
content = extractText(selected.content) || '[No text content]';
|
|
331
|
+
} else {
|
|
332
|
+
content = `Could not extract structured messages. Showing last 20 raw lines:\n${lines.slice(-20).join('\n')}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
agent: 'codex',
|
|
337
|
+
source: targetFile,
|
|
338
|
+
content: redactSensitiveText(content),
|
|
339
|
+
warnings,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readGeminiSession(id, chatsDir, cwd) {
|
|
344
|
+
const resolved = resolveGeminiTargetFile(id, chatsDir, cwd);
|
|
345
|
+
const targetFile = resolved.targetFile;
|
|
346
|
+
if (!targetFile) {
|
|
347
|
+
if (chatsDir) {
|
|
348
|
+
throw new Error(`No Gemini session found in ${normalizePath(chatsDir)}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const lines = ['No Gemini session found. Searched chats directories:'];
|
|
352
|
+
for (const dir of resolved.searchedDirs) {
|
|
353
|
+
lines.push(` - ${dir}`);
|
|
354
|
+
}
|
|
355
|
+
throw new Error(lines.join('\n'));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let session;
|
|
359
|
+
try {
|
|
360
|
+
session = JSON.parse(fs.readFileSync(targetFile, 'utf-8'));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new Error(`Failed to parse Gemini JSON: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let content = '';
|
|
366
|
+
if (Array.isArray(session.messages)) {
|
|
367
|
+
const selected =
|
|
368
|
+
[...session.messages].reverse().find(message => {
|
|
369
|
+
const type = (message.type || '').toLowerCase();
|
|
370
|
+
return type === 'gemini' || type === 'assistant' || type === 'model';
|
|
371
|
+
}) || session.messages[session.messages.length - 1];
|
|
372
|
+
|
|
373
|
+
if (!selected) {
|
|
374
|
+
throw new Error('Gemini session has no messages.');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
content = typeof selected.content === 'string'
|
|
378
|
+
? selected.content
|
|
379
|
+
: extractText(selected.content) || '[No text content]';
|
|
380
|
+
} else if (Array.isArray(session.history)) {
|
|
381
|
+
const selected =
|
|
382
|
+
[...session.history].reverse().find(turn => (turn.role || '').toLowerCase() !== 'user') ||
|
|
383
|
+
session.history[session.history.length - 1];
|
|
384
|
+
|
|
385
|
+
if (!selected) {
|
|
386
|
+
throw new Error('Gemini history is empty.');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (Array.isArray(selected.parts)) {
|
|
390
|
+
content = selected.parts.map(part => part.text || '').join('\n');
|
|
391
|
+
} else if (typeof selected.parts === 'string') {
|
|
392
|
+
content = selected.parts;
|
|
393
|
+
} else {
|
|
394
|
+
content = '[No text content]';
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
throw new Error('Unknown Gemini session schema. Supported fields: messages, history.');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
agent: 'gemini',
|
|
402
|
+
source: targetFile,
|
|
403
|
+
content: redactSensitiveText(content),
|
|
404
|
+
warnings: [],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function readClaudeSession(id, cwd) {
|
|
409
|
+
if (!fs.existsSync(claudeProjectsBase)) {
|
|
410
|
+
throw new Error(`Claude projects directory not found: ${claudeProjectsBase}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const warnings = [];
|
|
414
|
+
const targetFile = resolveClaudeTargetFile(id, cwd, warnings);
|
|
415
|
+
if (!targetFile) {
|
|
416
|
+
throw new Error('No Claude session found.');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const lines = readJsonlLines(targetFile);
|
|
420
|
+
const messages = [];
|
|
421
|
+
let skipped = 0;
|
|
422
|
+
|
|
423
|
+
for (const line of lines) {
|
|
424
|
+
try {
|
|
425
|
+
const json = JSON.parse(line);
|
|
426
|
+
const message = json.message || json;
|
|
427
|
+
if (json.type === 'assistant' || message.role === 'assistant') {
|
|
428
|
+
const content = message.content !== undefined ? message.content : json.content;
|
|
429
|
+
const text = extractClaudeText(content);
|
|
430
|
+
if (text) {
|
|
431
|
+
messages.push(text);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
} catch (error) {
|
|
435
|
+
skipped += 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (skipped > 0) {
|
|
440
|
+
warnings.push(`Warning: skipped ${skipped} unparseable line(s) in ${targetFile}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const content = messages.length > 0
|
|
444
|
+
? messages[messages.length - 1]
|
|
445
|
+
: `Could not extract assistant messages. Showing last 20 raw lines:\n${lines.slice(-20).join('\n')}`;
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
agent: 'claude',
|
|
449
|
+
source: targetFile,
|
|
450
|
+
content: redactSensitiveText(content),
|
|
451
|
+
warnings,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function readSource(sourceSpec, defaultCwd) {
|
|
456
|
+
const effectiveCwd = normalizePath(sourceSpec.cwd || defaultCwd);
|
|
457
|
+
if (sourceSpec.agent === 'codex') {
|
|
458
|
+
return readCodexSession(sourceSpec.session_id || null, effectiveCwd);
|
|
459
|
+
}
|
|
460
|
+
if (sourceSpec.agent === 'gemini') {
|
|
461
|
+
return readGeminiSession(sourceSpec.session_id || null, sourceSpec.chats_dir || null, effectiveCwd);
|
|
462
|
+
}
|
|
463
|
+
if (sourceSpec.agent === 'claude') {
|
|
464
|
+
return readClaudeSession(sourceSpec.session_id || null, effectiveCwd);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
throw new Error(`Unsupported agent: ${sourceSpec.agent}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function parseSourceArg(raw) {
|
|
471
|
+
const firstColon = raw.indexOf(':');
|
|
472
|
+
const agent = (firstColon === -1 ? raw : raw.slice(0, firstColon)).trim().toLowerCase();
|
|
473
|
+
const session = firstColon === -1 ? null : raw.slice(firstColon + 1).trim();
|
|
474
|
+
|
|
475
|
+
if (!['codex', 'gemini', 'claude'].includes(agent)) {
|
|
476
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
agent,
|
|
481
|
+
session_id: session ? session : null,
|
|
482
|
+
current_session: !session,
|
|
483
|
+
cwd: null,
|
|
484
|
+
chats_dir: null,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function evidenceTag(sourceSpec) {
|
|
489
|
+
const id = sourceSpec.session_id ? sourceSpec.session_id.slice(0, 8) : 'latest';
|
|
490
|
+
return `[${sourceSpec.agent}:${id}]`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function computeVerdict(mode, missingCount, uniqueCount, successCount) {
|
|
494
|
+
if (successCount === 0) return 'INCOMPLETE';
|
|
495
|
+
|
|
496
|
+
if (mode === 'verify') {
|
|
497
|
+
if (missingCount === 0 && uniqueCount <= 1) return 'PASS';
|
|
498
|
+
return 'FAIL';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (mode === 'steer') return 'STEERING_PLAN_READY';
|
|
502
|
+
if (mode === 'analyze') return 'ANALYSIS_COMPLETE';
|
|
503
|
+
if (mode === 'feedback') return 'FEEDBACK_COMPLETE';
|
|
504
|
+
return 'INCOMPLETE';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildReport(request, defaultCwd) {
|
|
508
|
+
const successful = [];
|
|
509
|
+
const missing = [];
|
|
510
|
+
|
|
511
|
+
for (const sourceSpec of request.sources) {
|
|
512
|
+
const evidence = evidenceTag(sourceSpec);
|
|
513
|
+
try {
|
|
514
|
+
const session = readSource(sourceSpec, defaultCwd);
|
|
515
|
+
successful.push({ sourceSpec, session, evidence });
|
|
516
|
+
} catch (error) {
|
|
517
|
+
missing.push({ sourceSpec, error: error.message || String(error), evidence });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const findings = [];
|
|
522
|
+
|
|
523
|
+
for (const item of missing) {
|
|
524
|
+
findings.push({
|
|
525
|
+
severity: 'P1',
|
|
526
|
+
summary: `Source unavailable: ${item.sourceSpec.agent} (${item.error})`,
|
|
527
|
+
evidence: [item.evidence],
|
|
528
|
+
confidence: 0.9,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
for (const item of successful) {
|
|
533
|
+
for (const warning of item.session.warnings || []) {
|
|
534
|
+
findings.push({
|
|
535
|
+
severity: 'P2',
|
|
536
|
+
summary: `Source warning: ${warning}`,
|
|
537
|
+
evidence: [item.evidence],
|
|
538
|
+
confidence: 0.75,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const uniqueContents = new Set(successful.map(item => (item.session.content || '').trim()));
|
|
544
|
+
|
|
545
|
+
if (successful.length >= 2) {
|
|
546
|
+
if (uniqueContents.size > 1) {
|
|
547
|
+
findings.push({
|
|
548
|
+
severity: 'P1',
|
|
549
|
+
summary: 'Divergent agent outputs detected',
|
|
550
|
+
evidence: successful.map(item => item.evidence),
|
|
551
|
+
confidence: 0.75,
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
findings.push({
|
|
555
|
+
severity: 'P3',
|
|
556
|
+
summary: 'All available agent outputs are aligned',
|
|
557
|
+
evidence: successful.map(item => item.evidence),
|
|
558
|
+
confidence: 0.9,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
findings.push({
|
|
563
|
+
severity: 'P2',
|
|
564
|
+
summary: 'Insufficient comparable sources',
|
|
565
|
+
evidence: successful.map(item => item.evidence),
|
|
566
|
+
confidence: 0.5,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const recommendedNextActions = [];
|
|
571
|
+
if (missing.length > 0) {
|
|
572
|
+
recommendedNextActions.push('Provide valid session identifiers or cwd values for unavailable sources.');
|
|
573
|
+
}
|
|
574
|
+
if (uniqueContents.size > 1) {
|
|
575
|
+
recommendedNextActions.push('Inspect full transcripts for diverging sources before final decisions.');
|
|
576
|
+
}
|
|
577
|
+
if (Array.isArray(request.constraints) && request.constraints.length > 0) {
|
|
578
|
+
recommendedNextActions.push(`Verify recommendations against constraints: ${request.constraints.join('; ')}.`);
|
|
579
|
+
}
|
|
580
|
+
if (recommendedNextActions.length === 0) {
|
|
581
|
+
recommendedNextActions.push('No immediate action required.');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const openQuestions = missing.map(item => `Missing source ${item.sourceSpec.agent}: ${item.error}`);
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
mode: request.mode,
|
|
588
|
+
task: request.task,
|
|
589
|
+
success_criteria: request.success_criteria,
|
|
590
|
+
sources_used: successful.map(item => `${item.evidence} ${item.session.source}`),
|
|
591
|
+
verdict: computeVerdict(request.mode, missing.length, uniqueContents.size, successful.length),
|
|
592
|
+
findings: findings,
|
|
593
|
+
recommended_next_actions: recommendedNextActions,
|
|
594
|
+
open_questions: openQuestions,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function renderReadResult(result, asJson) {
|
|
599
|
+
if (asJson) {
|
|
600
|
+
console.log(JSON.stringify(result, null, 2));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
for (const warning of result.warnings || []) {
|
|
605
|
+
console.error(warning);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const label = result.agent.charAt(0).toUpperCase() + result.agent.slice(1);
|
|
609
|
+
console.log(`SOURCE: ${label} Session (${result.source})`);
|
|
610
|
+
console.log('---');
|
|
611
|
+
console.log(result.content);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function renderReport(result, asJson) {
|
|
615
|
+
if (asJson) {
|
|
616
|
+
console.log(JSON.stringify(result, null, 2));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const lines = [];
|
|
621
|
+
lines.push('### Inter-Agent Coordinator Report');
|
|
622
|
+
lines.push('');
|
|
623
|
+
lines.push(`**Mode:** ${result.mode}`);
|
|
624
|
+
lines.push(`**Task:** ${result.task}`);
|
|
625
|
+
lines.push('**Success Criteria:**');
|
|
626
|
+
for (const criterion of result.success_criteria || []) {
|
|
627
|
+
lines.push(`- ${criterion}`);
|
|
628
|
+
}
|
|
629
|
+
lines.push('');
|
|
630
|
+
lines.push('**Sources Used:**');
|
|
631
|
+
for (const source of result.sources_used || []) {
|
|
632
|
+
lines.push(`- ${source}`);
|
|
633
|
+
}
|
|
634
|
+
lines.push('');
|
|
635
|
+
lines.push(`**Verdict:** ${result.verdict}`);
|
|
636
|
+
lines.push('');
|
|
637
|
+
lines.push('**Findings:**');
|
|
638
|
+
for (const finding of result.findings || []) {
|
|
639
|
+
lines.push(
|
|
640
|
+
`- **${finding.severity}:** ${finding.summary} (evidence: ${(finding.evidence || []).join(', ')}; confidence: ${Number(finding.confidence || 0).toFixed(2)})`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
lines.push('');
|
|
644
|
+
lines.push('**Recommended Next Actions:**');
|
|
645
|
+
(result.recommended_next_actions || []).forEach((action, index) => {
|
|
646
|
+
lines.push(`${index + 1}. ${action}`);
|
|
647
|
+
});
|
|
648
|
+
if ((result.open_questions || []).length > 0) {
|
|
649
|
+
lines.push('');
|
|
650
|
+
lines.push('**Open Questions:**');
|
|
651
|
+
for (const question of result.open_questions) {
|
|
652
|
+
lines.push(`- ${question}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
console.log(lines.join('\n'));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function validateMode(mode) {
|
|
660
|
+
const allowed = new Set(['verify', 'steer', 'analyze', 'feedback']);
|
|
661
|
+
if (!allowed.has(mode)) {
|
|
662
|
+
throw new Error(`Unsupported mode: ${mode}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function runRead(inputArgs) {
|
|
667
|
+
const agent = getOptionValue(inputArgs, '--agent', 'codex');
|
|
668
|
+
const id = getOptionValue(inputArgs, '--id', null);
|
|
669
|
+
const chatsDir = getOptionValue(inputArgs, '--chats-dir', null);
|
|
670
|
+
const cwd = normalizePath(getOptionValue(inputArgs, '--cwd', process.cwd()));
|
|
671
|
+
const asJson = hasFlag(inputArgs, '--json');
|
|
672
|
+
|
|
673
|
+
let result;
|
|
674
|
+
if (agent === 'codex') {
|
|
675
|
+
result = readCodexSession(id, cwd);
|
|
676
|
+
} else if (agent === 'gemini') {
|
|
677
|
+
result = readGeminiSession(id, chatsDir, cwd);
|
|
678
|
+
} else if (agent === 'claude') {
|
|
679
|
+
result = readClaudeSession(id, cwd);
|
|
680
|
+
} else {
|
|
681
|
+
throw new Error(`Unknown agent: ${agent}. Supported: codex, gemini, claude`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
renderReadResult(result, asJson);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function runCompare(inputArgs) {
|
|
688
|
+
const sourcesRaw = getOptionValues(inputArgs, '--source');
|
|
689
|
+
if (sourcesRaw.length === 0) {
|
|
690
|
+
throw new Error('compare requires at least one --source option');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const cwd = normalizePath(getOptionValue(inputArgs, '--cwd', process.cwd()));
|
|
694
|
+
const asJson = hasFlag(inputArgs, '--json');
|
|
695
|
+
const sourceSpecs = sourcesRaw.map(parseSourceArg);
|
|
696
|
+
|
|
697
|
+
const report = buildReport(
|
|
698
|
+
{
|
|
699
|
+
mode: 'analyze',
|
|
700
|
+
task: 'Compare agent outputs',
|
|
701
|
+
success_criteria: [
|
|
702
|
+
'Identify agreements and contradictions',
|
|
703
|
+
'Highlight unavailable sources',
|
|
704
|
+
],
|
|
705
|
+
sources: sourceSpecs,
|
|
706
|
+
constraints: [],
|
|
707
|
+
},
|
|
708
|
+
cwd
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
renderReport(report, asJson);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function runReport(inputArgs) {
|
|
715
|
+
const handoffPath = getOptionValue(inputArgs, '--handoff', null);
|
|
716
|
+
if (!handoffPath) {
|
|
717
|
+
throw new Error('report requires --handoff=<path>');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const cwd = normalizePath(getOptionValue(inputArgs, '--cwd', process.cwd()));
|
|
721
|
+
const asJson = hasFlag(inputArgs, '--json');
|
|
722
|
+
|
|
723
|
+
let handoff;
|
|
724
|
+
try {
|
|
725
|
+
handoff = JSON.parse(fs.readFileSync(normalizePath(handoffPath), 'utf-8'));
|
|
726
|
+
} catch (error) {
|
|
727
|
+
throw new Error(`Failed to read handoff JSON: ${error.message}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const mode = String(handoff.mode || '').toLowerCase();
|
|
731
|
+
validateMode(mode);
|
|
732
|
+
|
|
733
|
+
if (typeof handoff.task !== 'string' || !handoff.task.trim()) {
|
|
734
|
+
throw new Error('Handoff is missing required string field: task');
|
|
735
|
+
}
|
|
736
|
+
if (!Array.isArray(handoff.success_criteria) || handoff.success_criteria.length === 0) {
|
|
737
|
+
throw new Error('Handoff is missing required array field: success_criteria');
|
|
738
|
+
}
|
|
739
|
+
if (!Array.isArray(handoff.sources) || handoff.sources.length === 0) {
|
|
740
|
+
throw new Error('Handoff is missing required array field: sources');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const sourceSpecs = handoff.sources.map(source => {
|
|
744
|
+
const agent = String(source.agent || '').toLowerCase();
|
|
745
|
+
if (!['codex', 'gemini', 'claude'].includes(agent)) {
|
|
746
|
+
throw new Error(`Unsupported agent: ${agent}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const sessionId = typeof source.session_id === 'string' && source.session_id.trim()
|
|
750
|
+
? source.session_id.trim()
|
|
751
|
+
: null;
|
|
752
|
+
const currentSession = source.current_session === true;
|
|
753
|
+
|
|
754
|
+
if (!sessionId && !currentSession) {
|
|
755
|
+
throw new Error('Each source must provide session_id or set current_session=true');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return {
|
|
759
|
+
agent,
|
|
760
|
+
session_id: sessionId,
|
|
761
|
+
current_session: currentSession,
|
|
762
|
+
cwd: typeof source.cwd === 'string' && source.cwd.trim() ? source.cwd : null,
|
|
763
|
+
chats_dir: null,
|
|
764
|
+
};
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const report = buildReport(
|
|
768
|
+
{
|
|
769
|
+
mode,
|
|
770
|
+
task: handoff.task,
|
|
771
|
+
success_criteria: handoff.success_criteria.map(String),
|
|
772
|
+
sources: sourceSpecs,
|
|
773
|
+
constraints: Array.isArray(handoff.constraints) ? handoff.constraints.map(String) : [],
|
|
774
|
+
},
|
|
775
|
+
cwd
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
renderReport(report, asJson);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
if (command === 'read') {
|
|
783
|
+
runRead(args);
|
|
784
|
+
} else if (command === 'compare') {
|
|
785
|
+
runCompare(args);
|
|
786
|
+
} else if (command === 'report') {
|
|
787
|
+
runReport(args);
|
|
788
|
+
} else {
|
|
789
|
+
throw new Error(`Unknown command: ${command}`);
|
|
790
|
+
}
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error(error.message || String(error));
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|