@binarycheater/research-sidecar 0.1.0 → 0.1.2
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/dist/client/assets/{index-BpVgCKdz.css → index-C3mAbt-i.css} +1 -1
- package/dist/client/assets/index-DTKJmZxM.js +321 -0
- package/dist/client/index.html +2 -2
- package/dist-server/lib/files.js +71 -0
- package/dist-server/lib/researchGraph.js +76 -0
- package/dist-server/server/index.js +17 -3
- package/package.json +1 -1
- package/skills/sidecar-thinking/SKILL.md +54 -14
- package/skills/sidecar-thinking/agents/openai.yaml +2 -2
- package/dist/client/assets/index-D7VDrQ1Q.js +0 -324
package/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Research Sidecar</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DTKJmZxM.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C3mAbt-i.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/dist-server/lib/files.js
CHANGED
|
@@ -77,6 +77,40 @@ export async function readWorkspaceFile(workspaceRoot, requestedPath) {
|
|
|
77
77
|
addedAt: new Date().toISOString()
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
export async function readWorkspaceRawFile(workspaceRoot, requestedPath) {
|
|
81
|
+
const fullPath = resolveWorkspacePath(workspaceRoot, requestedPath);
|
|
82
|
+
const info = await stat(fullPath);
|
|
83
|
+
if (!info.isFile()) {
|
|
84
|
+
throw new Error("Requested path is not a file.");
|
|
85
|
+
}
|
|
86
|
+
const content = await readFile(fullPath);
|
|
87
|
+
return {
|
|
88
|
+
path: requestedPath,
|
|
89
|
+
content,
|
|
90
|
+
bytes: content.byteLength,
|
|
91
|
+
mimeType: detectRawMimeType(requestedPath)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function resolveWorkspaceFileForOpen(workspaceRoot, requestedPath) {
|
|
95
|
+
const fullPath = resolveWorkspacePath(workspaceRoot, requestedPath);
|
|
96
|
+
const info = await stat(fullPath);
|
|
97
|
+
if (!info.isFile()) {
|
|
98
|
+
throw new Error("Requested path is not a file.");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
path: requestedPath,
|
|
102
|
+
fullPath
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function getWorkspaceOpenCommand(fullPath, platform = process.platform) {
|
|
106
|
+
if (platform === "darwin") {
|
|
107
|
+
return { command: "open", args: [fullPath] };
|
|
108
|
+
}
|
|
109
|
+
if (platform === "win32") {
|
|
110
|
+
return { command: "cmd", args: ["/c", "start", "", fullPath] };
|
|
111
|
+
}
|
|
112
|
+
return { command: "xdg-open", args: [fullPath] };
|
|
113
|
+
}
|
|
80
114
|
export async function writeWorkspaceFile(workspaceRoot, requestedPath, content, allowedExtensions) {
|
|
81
115
|
assertWritableWorkspacePath(requestedPath, allowedExtensions);
|
|
82
116
|
const fullPath = resolveWorkspacePath(workspaceRoot, requestedPath);
|
|
@@ -97,6 +131,43 @@ export function detectFilePreviewFormat(path) {
|
|
|
97
131
|
}
|
|
98
132
|
return { format: "text", mimeType: "text/plain" };
|
|
99
133
|
}
|
|
134
|
+
function detectRawMimeType(path) {
|
|
135
|
+
const extension = extname(path).toLowerCase();
|
|
136
|
+
switch (extension) {
|
|
137
|
+
case ".apng":
|
|
138
|
+
return "image/apng";
|
|
139
|
+
case ".avif":
|
|
140
|
+
return "image/avif";
|
|
141
|
+
case ".gif":
|
|
142
|
+
return "image/gif";
|
|
143
|
+
case ".jpg":
|
|
144
|
+
case ".jpeg":
|
|
145
|
+
return "image/jpeg";
|
|
146
|
+
case ".png":
|
|
147
|
+
return "image/png";
|
|
148
|
+
case ".svg":
|
|
149
|
+
return "image/svg+xml";
|
|
150
|
+
case ".webp":
|
|
151
|
+
return "image/webp";
|
|
152
|
+
case ".html":
|
|
153
|
+
case ".htm":
|
|
154
|
+
return "text/html; charset=utf-8";
|
|
155
|
+
case ".md":
|
|
156
|
+
case ".markdown":
|
|
157
|
+
return "text/markdown; charset=utf-8";
|
|
158
|
+
case ".css":
|
|
159
|
+
return "text/css; charset=utf-8";
|
|
160
|
+
case ".js":
|
|
161
|
+
case ".mjs":
|
|
162
|
+
return "text/javascript; charset=utf-8";
|
|
163
|
+
case ".json":
|
|
164
|
+
return "application/json; charset=utf-8";
|
|
165
|
+
case ".pdf":
|
|
166
|
+
return "application/pdf";
|
|
167
|
+
default:
|
|
168
|
+
return "application/octet-stream";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
100
171
|
function assertWritableWorkspacePath(path, allowedExtensions) {
|
|
101
172
|
const extension = extname(path).toLowerCase();
|
|
102
173
|
if (CODE_EXTENSIONS.has(extension)) {
|
|
@@ -67,9 +67,57 @@ export function getExpandableDescendantIds(graph, rootId) {
|
|
|
67
67
|
}
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
|
+
export function getNextExpandedIdsForNodeClick(graph, currentIds, nodeId) {
|
|
71
|
+
const next = new Set(currentIds);
|
|
72
|
+
const descendantIds = getHierarchyDescendantIds(graph, nodeId);
|
|
73
|
+
if (next.has(nodeId)) {
|
|
74
|
+
next.delete(nodeId);
|
|
75
|
+
for (const descendantId of descendantIds) {
|
|
76
|
+
next.delete(descendantId);
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
next.add(nodeId);
|
|
81
|
+
for (const descendantId of descendantIds) {
|
|
82
|
+
next.delete(descendantId);
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
export function getFocusedResearchGraph(graph, options) {
|
|
87
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
88
|
+
const focus = byId.get(options.focusId);
|
|
89
|
+
if (!focus) {
|
|
90
|
+
return { rootId: graph.rootId, nodes: [], edges: [], childCounts: {} };
|
|
91
|
+
}
|
|
92
|
+
const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
|
|
93
|
+
const childrenByParent = groupEdges(hierarchyEdges, "from");
|
|
94
|
+
const parentsByChild = groupEdges(hierarchyEdges, "to");
|
|
95
|
+
const childCounts = Object.fromEntries(graph.nodes.map((node) => [node.id, childrenByParent.get(node.id)?.length || 0]));
|
|
96
|
+
const visibleIds = new Set([options.focusId]);
|
|
97
|
+
for (const edge of graph.edges) {
|
|
98
|
+
if (edge.from === options.focusId)
|
|
99
|
+
visibleIds.add(edge.to);
|
|
100
|
+
if (edge.to === options.focusId)
|
|
101
|
+
visibleIds.add(edge.from);
|
|
102
|
+
}
|
|
103
|
+
if (options.includeAntecedents) {
|
|
104
|
+
for (const ancestorId of getAncestorIds(options.focusId, parentsByChild)) {
|
|
105
|
+
visibleIds.add(ancestorId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const nodes = graph.nodes.filter((node) => visibleIds.has(node.id));
|
|
109
|
+
const edges = graph.edges.filter((edge) => visibleIds.has(edge.from) && visibleIds.has(edge.to));
|
|
110
|
+
return {
|
|
111
|
+
rootId: focus.id,
|
|
112
|
+
nodes,
|
|
113
|
+
edges,
|
|
114
|
+
childCounts
|
|
115
|
+
};
|
|
116
|
+
}
|
|
70
117
|
export function layoutResearchGraph(graph, options) {
|
|
71
118
|
const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
|
|
72
119
|
const childrenByParent = groupEdges(hierarchyEdges, "from");
|
|
120
|
+
const parentsByChild = groupEdges(hierarchyEdges, "to");
|
|
73
121
|
const depthById = new Map([[graph.rootId, 0]]);
|
|
74
122
|
const queue = [graph.rootId];
|
|
75
123
|
for (let index = 0; index < queue.length; index += 1) {
|
|
@@ -82,6 +130,17 @@ export function layoutResearchGraph(graph, options) {
|
|
|
82
130
|
queue.push(edge.to);
|
|
83
131
|
}
|
|
84
132
|
}
|
|
133
|
+
const parentQueue = [graph.rootId];
|
|
134
|
+
for (let index = 0; index < parentQueue.length; index += 1) {
|
|
135
|
+
const childId = parentQueue[index];
|
|
136
|
+
const childDepth = depthById.get(childId) || 0;
|
|
137
|
+
for (const edge of parentsByChild.get(childId) || []) {
|
|
138
|
+
if (depthById.has(edge.from))
|
|
139
|
+
continue;
|
|
140
|
+
depthById.set(edge.from, childDepth - 1);
|
|
141
|
+
parentQueue.push(edge.from);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
85
144
|
const rows = new Map();
|
|
86
145
|
for (const node of graph.nodes) {
|
|
87
146
|
const depth = depthById.get(node.id) ?? 0;
|
|
@@ -114,6 +173,23 @@ function collectExpandedTree(id, childrenByParent, expandedIds, visibleIds) {
|
|
|
114
173
|
collectExpandedTree(edge.to, childrenByParent, expandedIds, visibleIds);
|
|
115
174
|
}
|
|
116
175
|
}
|
|
176
|
+
function getHierarchyDescendantIds(graph, rootId) {
|
|
177
|
+
const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
|
|
178
|
+
const childrenByParent = groupEdges(hierarchyEdges, "from");
|
|
179
|
+
const result = [];
|
|
180
|
+
const queue = [rootId];
|
|
181
|
+
const seen = new Set([rootId]);
|
|
182
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
183
|
+
for (const edge of childrenByParent.get(queue[index]) || []) {
|
|
184
|
+
if (seen.has(edge.to))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(edge.to);
|
|
187
|
+
result.push(edge.to);
|
|
188
|
+
queue.push(edge.to);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
117
193
|
function getAncestorIds(id, parentsByChild) {
|
|
118
194
|
const ancestors = [];
|
|
119
195
|
const seen = new Set();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { buildContextPacket } from "../lib/context.js";
|
|
5
|
-
import { readWorkspaceFile } from "../lib/files.js";
|
|
6
|
+
import { getWorkspaceOpenCommand, readWorkspaceFile, readWorkspaceRawFile, resolveWorkspaceFileForOpen } from "../lib/files.js";
|
|
6
7
|
import { discoverGraphManifests } from "../lib/graphDiscovery.js";
|
|
7
8
|
import { DEFAULT_REVIEW_PROMPT } from "../lib/prompt.js";
|
|
8
9
|
import { loadResearchGraphManifest } from "../lib/researchGraphManifest.js";
|
|
@@ -75,7 +76,7 @@ app.get("/api/workspace/file", async (req, res, next) => {
|
|
|
75
76
|
app.get("/api/workspace/raw", async (req, res, next) => {
|
|
76
77
|
try {
|
|
77
78
|
const path = String(req.query.path || "");
|
|
78
|
-
const snapshot = await
|
|
79
|
+
const snapshot = await readWorkspaceRawFile(config.workspaceRoot, path);
|
|
79
80
|
res.type(snapshot.mimeType).send(snapshot.content);
|
|
80
81
|
}
|
|
81
82
|
catch (error) {
|
|
@@ -85,13 +86,26 @@ app.get("/api/workspace/raw", async (req, res, next) => {
|
|
|
85
86
|
app.get(/^\/api\/workspace\/raw-path\/(.+)$/, async (req, res, next) => {
|
|
86
87
|
try {
|
|
87
88
|
const path = decodeURIComponent(String(req.params[0] || ""));
|
|
88
|
-
const snapshot = await
|
|
89
|
+
const snapshot = await readWorkspaceRawFile(config.workspaceRoot, path);
|
|
89
90
|
res.type(snapshot.mimeType).send(snapshot.content);
|
|
90
91
|
}
|
|
91
92
|
catch (error) {
|
|
92
93
|
next(error);
|
|
93
94
|
}
|
|
94
95
|
});
|
|
96
|
+
app.post("/api/workspace/open", async (req, res, next) => {
|
|
97
|
+
try {
|
|
98
|
+
const target = await resolveWorkspaceFileForOpen(config.workspaceRoot, String(req.body?.path || ""));
|
|
99
|
+
const { command, args } = getWorkspaceOpenCommand(target.fullPath);
|
|
100
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
101
|
+
child.on("error", () => undefined);
|
|
102
|
+
child.unref();
|
|
103
|
+
res.json({ opened: true, path: target.path });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
next(error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
95
109
|
app.get("/api/research-graph", async (_req, res, next) => {
|
|
96
110
|
try {
|
|
97
111
|
res.json(await loadResearchGraphManifest(config.workspaceRoot, config.graphManifestPath));
|
package/package.json
CHANGED
|
@@ -1,40 +1,80 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sidecar-thinking
|
|
3
|
-
description: Use when
|
|
3
|
+
description: Use when the user explicitly asks to use Research Sidecar, open/start/connect to the sidecar app, inspect or select a sidecar research graph, install sidecar workspace skills, or run an external/second-opinion sidecar thinking pass.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Sidecar Thinking
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Research Sidecar is an opt-in external tool. Do not invoke it just because the task involves research, reports, Markdown, YAML, or graph-like structure.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Strict Trigger
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Use this skill only when the user explicitly asks for at least one of these:
|
|
13
|
+
|
|
14
|
+
- Use, open, start, run, connect to, or inspect Research Sidecar.
|
|
15
|
+
- Use the sidecar graph view, select/save a graph, preview graph-linked documents, or inspect a sidecar research graph.
|
|
16
|
+
- Do an external, out-of-band, second-opinion, or sidecar thinking pass.
|
|
17
|
+
- Install the sidecar bundled skills into the current workspace.
|
|
18
|
+
|
|
19
|
+
Do not use this skill for ordinary research reasoning, report writing, code edits, Markdown/HTML generation, YAML editing, or generic graph work unless the user ties the request to Sidecar.
|
|
20
|
+
|
|
21
|
+
## Current CLI Shape
|
|
22
|
+
|
|
23
|
+
Start the app only when the user asked for Sidecar to be used or started:
|
|
13
24
|
|
|
14
25
|
```bash
|
|
15
26
|
research-sidecar
|
|
16
27
|
```
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
Useful variants:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
research-sidecar --workspace /path/to/workspace
|
|
33
|
+
research-sidecar --graph dingyi/synthetic/graph.yaml
|
|
34
|
+
research-sidecar --port 4317
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Initialize a workspace:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
research-sidecar init --graph research/graph.yaml
|
|
41
|
+
research-sidecar init --graph research/graph.yaml --no-skills
|
|
42
|
+
```
|
|
19
43
|
|
|
20
|
-
|
|
44
|
+
Install bundled skills into the workspace:
|
|
21
45
|
|
|
22
|
-
|
|
46
|
+
```bash
|
|
47
|
+
research-sidecar install-skills
|
|
48
|
+
research-sidecar install-skills --force
|
|
49
|
+
```
|
|
23
50
|
|
|
24
|
-
|
|
51
|
+
If installed as a project dependency, use the same commands through `npx`:
|
|
25
52
|
|
|
26
53
|
```bash
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
npx research-sidecar
|
|
55
|
+
npx research-sidecar install-skills
|
|
29
56
|
```
|
|
30
57
|
|
|
31
|
-
|
|
58
|
+
For one-shot use without a local install:
|
|
32
59
|
|
|
33
60
|
```bash
|
|
34
|
-
|
|
35
|
-
npm run codex:ask -- --title "Review" --context "Codex summary..." --question "What should Codex do next?"
|
|
61
|
+
npx @binarycheater/research-sidecar
|
|
36
62
|
```
|
|
37
63
|
|
|
64
|
+
The directory where `research-sidecar` runs is the workspace root unless `--workspace` is supplied. Private state lives in `.side/config.json`. Default URL: `http://localhost:4317`.
|
|
65
|
+
|
|
66
|
+
## Sessions And API
|
|
67
|
+
|
|
68
|
+
Use the API only after the Sidecar server is running and the user asked for Sidecar involvement.
|
|
69
|
+
|
|
70
|
+
Common session sequence:
|
|
71
|
+
|
|
72
|
+
1. `POST /api/sessions`
|
|
73
|
+
2. `PATCH /api/sessions/:id` with manual context
|
|
74
|
+
3. `POST /api/sessions/:id/files` for workspace-relative files
|
|
75
|
+
4. `POST /api/sessions/:id/messages` to stage a user question
|
|
76
|
+
5. `POST /api/sessions/:id/stream` only if the user asked Codex to relay the sidecar answer
|
|
77
|
+
|
|
38
78
|
## Research Graph
|
|
39
79
|
|
|
40
80
|
The graph is manifest-first:
|
|
@@ -44,7 +84,7 @@ The graph is manifest-first:
|
|
|
44
84
|
- Markdown frontmatter can supply local metadata, but `graph.yaml` wins on conflicts.
|
|
45
85
|
- Graph file links are relative to the graph manifest directory by default. Use a leading `/` for an explicit workspace-root-relative link.
|
|
46
86
|
|
|
47
|
-
Default manifest path: `research/graph.yaml`. Override with
|
|
87
|
+
Default manifest path: `research/graph.yaml`. Override with `--graph`, or save the active graph in `.side/config.json` at `graph.manifestPath`. The UI can discover graph files across the workspace and save the selected graph to `.side/config.json`.
|
|
48
88
|
|
|
49
89
|
## API Quick Reference
|
|
50
90
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Sidecar Thinking"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "Use $sidecar-thinking to
|
|
3
|
+
short_description: "Use Research Sidecar only when explicitly requested"
|
|
4
|
+
default_prompt: "Use $sidecar-thinking only when I explicitly ask to use/open/start Research Sidecar, inspect a sidecar research graph, install sidecar workspace skills, or run an external sidecar thinking pass."
|