hypercore-cli 1.1.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/README.md +110 -0
- package/dist/api-XGC7D5AW.js +162 -0
- package/dist/auth-DNQWYQKT.js +21 -0
- package/dist/background-2EGCAAQH.js +14 -0
- package/dist/backlog-Q2NZCLNY.js +24 -0
- package/dist/chunk-2CMSCWQW.js +162 -0
- package/dist/chunk-2LJ2DVEB.js +167 -0
- package/dist/chunk-3RPFCQKJ.js +288 -0
- package/dist/chunk-43OLRXM5.js +263 -0
- package/dist/chunk-4DVYJAJL.js +57 -0
- package/dist/chunk-6OL3GA3P.js +173 -0
- package/dist/chunk-AUHU7ALH.js +2023 -0
- package/dist/chunk-B6A2AKLN.js +139 -0
- package/dist/chunk-BE46C7JW.js +46 -0
- package/dist/chunk-CUVAUOXL.js +58 -0
- package/dist/chunk-GH7E2OJE.js +223 -0
- package/dist/chunk-GOOTEPBK.js +271 -0
- package/dist/chunk-GPPMJYSM.js +133 -0
- package/dist/chunk-GU2FZQ6A.js +69 -0
- package/dist/chunk-IOPKN5GD.js +190 -0
- package/dist/chunk-IXOIOGR5.js +1505 -0
- package/dist/chunk-KRPOPWGA.js +251 -0
- package/dist/chunk-MGLJ53QN.js +219 -0
- package/dist/chunk-MV4TTRYX.js +533 -0
- package/dist/chunk-OPZYEVYR.js +150 -0
- package/dist/chunk-QTSLP47C.js +166 -0
- package/dist/chunk-R3GPQC7I.js +393 -0
- package/dist/chunk-RKB2JOV2.js +43 -0
- package/dist/chunk-RNG3K465.js +80 -0
- package/dist/chunk-TGTYKBGC.js +86 -0
- package/dist/chunk-U5SGAIMM.js +681 -0
- package/dist/chunk-V5UHPPSY.js +140 -0
- package/dist/chunk-WHLVZCQY.js +245 -0
- package/dist/chunk-XDRCBMZZ.js +66 -0
- package/dist/chunk-XOS6HPEF.js +134 -0
- package/dist/chunk-ZSBHUGWR.js +262 -0
- package/dist/claude-NSQ442XD.js +12 -0
- package/dist/commands-CK3WFAGI.js +128 -0
- package/dist/commands-U63OEO5J.js +1044 -0
- package/dist/commands-ZE6GD3WC.js +232 -0
- package/dist/config-4EW42BSF.js +8 -0
- package/dist/config-loader-SXO674TF.js +24 -0
- package/dist/diagnose-AFW3ZTZ4.js +12 -0
- package/dist/display-IIUBEYWN.js +58 -0
- package/dist/extractor-QV53W2YJ.js +129 -0
- package/dist/history-WMSCHERZ.js +180 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +406 -0
- package/dist/instance-registry-YSIJXSO7.js +15 -0
- package/dist/keybindings-JAAMLH3G.js +15 -0
- package/dist/loader-WHNTZTLP.js +58 -0
- package/dist/network-MM6YWPGO.js +279 -0
- package/dist/notify-HPTALZDC.js +14 -0
- package/dist/openai-compat-UQWJXBEK.js +12 -0
- package/dist/permissions-JUKXMNDH.js +10 -0
- package/dist/prompt-QV45TXRL.js +166 -0
- package/dist/quality-ST7PPNFR.js +16 -0
- package/dist/repl-RT3AHL7M.js +3375 -0
- package/dist/roadmap-5OBEKROY.js +17 -0
- package/dist/server-PORT7OEG.js +57 -0
- package/dist/session-4VUNDWLH.js +21 -0
- package/dist/skills-V4A35XKG.js +175 -0
- package/dist/store-Y4LU5QTO.js +25 -0
- package/dist/team-HO7Z4SIM.js +385 -0
- package/dist/telemetry-6R4EIE6O.js +30 -0
- package/dist/test-runner-ZQH5Y6OJ.js +619 -0
- package/dist/theme-3SYJ3UQA.js +14 -0
- package/dist/upgrade-7TGI3SXO.js +83 -0
- package/dist/verify-JUDKTPKZ.js +14 -0
- package/dist/web/static/app.js +562 -0
- package/dist/web/static/index.html +132 -0
- package/dist/web/static/mirror.css +1001 -0
- package/dist/web/static/mirror.html +184 -0
- package/dist/web/static/mirror.js +1125 -0
- package/dist/web/static/onboard.css +302 -0
- package/dist/web/static/onboard.html +140 -0
- package/dist/web/static/onboard.js +260 -0
- package/dist/web/static/style.css +602 -0
- package/dist/web/static/workspace.css +1568 -0
- package/dist/web/static/workspace.html +408 -0
- package/dist/web/static/workspace.js +1683 -0
- package/dist/web-Z5HSCQHW.js +39 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 V.W.
|
|
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/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ⚡ Hypercore CLI
|
|
2
|
+
|
|
3
|
+
AI-Native CLI Shell for productivity — multi-model, production lines, MCP tools, team collaboration.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g hypercore-cli
|
|
9
|
+
hyper
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or try without installing:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx hypercore-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Multi-Model** — Claude, DeepSeek, Gemini, MiniMax, any OpenAI-compatible API
|
|
21
|
+
- **Production Lines** — chain AI agents into automated workflows (YAML config)
|
|
22
|
+
- **MCP Tools** — extend with Model Context Protocol servers
|
|
23
|
+
- **Web Dashboard** — monitor sessions, run lines, view history
|
|
24
|
+
- **Workspace** — browser-based GUI synced with terminal in real-time
|
|
25
|
+
- **Team Mode** — collaborate with teammates via LAN WebSocket
|
|
26
|
+
- **Hyper Network** — skill/need matching across team members
|
|
27
|
+
- **Memory System** — persistent memory extraction across sessions
|
|
28
|
+
- **Hook System** — lifecycle hooks (onSessionStart, onSessionEnd, etc.)
|
|
29
|
+
- **Git-Aware** — branch display, status integration
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
Create `~/.hypercore/config.toml`:
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
[model]
|
|
37
|
+
provider = "deepseek" # anthropic | deepseek | gemini | minimax | openai-compatible
|
|
38
|
+
model = "deepseek-coder"
|
|
39
|
+
apiKey = "sk-..."
|
|
40
|
+
|
|
41
|
+
# Optional: OpenAI-compatible endpoint
|
|
42
|
+
# baseURL = "https://api.example.com/v1"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Supported Providers
|
|
46
|
+
|
|
47
|
+
| Provider | Models | Config |
|
|
48
|
+
|----------|--------|--------|
|
|
49
|
+
| Anthropic | Claude Opus, Sonnet, Haiku | `provider = "anthropic"` |
|
|
50
|
+
| DeepSeek | DeepSeek Coder, Chat | `provider = "deepseek"` |
|
|
51
|
+
| Google | Gemini Pro, Flash | `provider = "gemini"` |
|
|
52
|
+
| MiniMax | MiniMax models | `provider = "minimax"` |
|
|
53
|
+
| Any OpenAI-compatible | Via base URL | `provider = "openai-compatible"` |
|
|
54
|
+
|
|
55
|
+
## Commands
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
|---------|-------------|
|
|
59
|
+
| `/dashboard` | Open web dashboard |
|
|
60
|
+
| `/workspace` | Open workspace GUI |
|
|
61
|
+
| `/run <line>` | Run a production line |
|
|
62
|
+
| `/new` | Start new session |
|
|
63
|
+
| `/save` | Save current session |
|
|
64
|
+
| `/cost` | Show token usage and cost |
|
|
65
|
+
| `/memory` | View memory system |
|
|
66
|
+
| `/compact` | Compact conversation context |
|
|
67
|
+
| `/team create` | Create a team |
|
|
68
|
+
| `/team join` | Join a team |
|
|
69
|
+
| `exit` | Exit |
|
|
70
|
+
|
|
71
|
+
## Production Lines
|
|
72
|
+
|
|
73
|
+
Define automated workflows in `.hypercore/lines/`:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
name: code-review
|
|
77
|
+
description: Automated code review
|
|
78
|
+
stations:
|
|
79
|
+
- name: analyze
|
|
80
|
+
agent: claude
|
|
81
|
+
prompt: "Review the following code for bugs and improvements..."
|
|
82
|
+
- name: summarize
|
|
83
|
+
agent: claude
|
|
84
|
+
prompt: "Summarize the review findings..."
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run with:
|
|
88
|
+
```bash
|
|
89
|
+
hyper run code-review
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## MCP Tools
|
|
93
|
+
|
|
94
|
+
Add MCP servers in `.hypercore/config.toml`:
|
|
95
|
+
|
|
96
|
+
```toml
|
|
97
|
+
[[mcp]]
|
|
98
|
+
name = "filesystem"
|
|
99
|
+
command = "npx"
|
|
100
|
+
args = ["-y", "@anthropic-ai/mcp-filesystem"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Requirements
|
|
104
|
+
|
|
105
|
+
- Node.js >= 18.0.0
|
|
106
|
+
- An API key for at least one supported provider
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
executeRound,
|
|
3
|
+
gatherProfile,
|
|
4
|
+
getLatestRound,
|
|
5
|
+
listRounds,
|
|
6
|
+
loadAllProfiles,
|
|
7
|
+
loadNetworkConfig,
|
|
8
|
+
loadRound,
|
|
9
|
+
saveNetworkConfig,
|
|
10
|
+
updateProfileField
|
|
11
|
+
} from "./chunk-U5SGAIMM.js";
|
|
12
|
+
import {
|
|
13
|
+
parseBody,
|
|
14
|
+
sendError,
|
|
15
|
+
sendJSON
|
|
16
|
+
} from "./chunk-AUHU7ALH.js";
|
|
17
|
+
import "./chunk-2LJ2DVEB.js";
|
|
18
|
+
import {
|
|
19
|
+
loadTeam
|
|
20
|
+
} from "./chunk-6OL3GA3P.js";
|
|
21
|
+
import "./chunk-XOS6HPEF.js";
|
|
22
|
+
import {
|
|
23
|
+
validateToken
|
|
24
|
+
} from "./chunk-XDRCBMZZ.js";
|
|
25
|
+
import "./chunk-MV4TTRYX.js";
|
|
26
|
+
import "./chunk-IXOIOGR5.js";
|
|
27
|
+
import "./chunk-KRPOPWGA.js";
|
|
28
|
+
import "./chunk-GU2FZQ6A.js";
|
|
29
|
+
import "./chunk-R3GPQC7I.js";
|
|
30
|
+
import "./chunk-BE46C7JW.js";
|
|
31
|
+
import "./chunk-RNG3K465.js";
|
|
32
|
+
import "./chunk-43OLRXM5.js";
|
|
33
|
+
import "./chunk-GOOTEPBK.js";
|
|
34
|
+
import "./chunk-B6A2AKLN.js";
|
|
35
|
+
import "./chunk-V5UHPPSY.js";
|
|
36
|
+
import "./chunk-WHLVZCQY.js";
|
|
37
|
+
import "./chunk-TGTYKBGC.js";
|
|
38
|
+
|
|
39
|
+
// src/network/api.ts
|
|
40
|
+
function getToken(req) {
|
|
41
|
+
const auth = req.headers.authorization;
|
|
42
|
+
if (!auth?.startsWith("Bearer ")) return null;
|
|
43
|
+
return auth.slice(7);
|
|
44
|
+
}
|
|
45
|
+
async function authenticate(req, teamId) {
|
|
46
|
+
const token = getToken(req);
|
|
47
|
+
if (!token) return null;
|
|
48
|
+
const team = await loadTeam(teamId);
|
|
49
|
+
if (!team) return null;
|
|
50
|
+
const member = validateToken(team, token);
|
|
51
|
+
if (!member) return null;
|
|
52
|
+
return { memberId: member.id, memberName: member.name };
|
|
53
|
+
}
|
|
54
|
+
async function handleNetworkApiRequest(req, res, config, pathname) {
|
|
55
|
+
const method = req.method || "GET";
|
|
56
|
+
const parts = pathname.replace("/api/network/", "").split("/");
|
|
57
|
+
const teamId = parts[0];
|
|
58
|
+
const resource = parts[1];
|
|
59
|
+
const subResource = parts[2];
|
|
60
|
+
if (!teamId) {
|
|
61
|
+
sendError(res, "\u7F3A\u5C11 teamId", 400);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const auth = await authenticate(req, teamId);
|
|
65
|
+
if (!auth) {
|
|
66
|
+
sendError(res, "\u8BA4\u8BC1\u5931\u8D25", 401);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (resource === "profile" && !subResource && method === "GET") {
|
|
70
|
+
const profile = await gatherProfile(teamId, auth.memberId, auth.memberName);
|
|
71
|
+
sendJSON(res, { profile });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (resource === "profile" && !subResource && method === "PUT") {
|
|
75
|
+
const body = await parseBody(req);
|
|
76
|
+
const field = body.field;
|
|
77
|
+
const value = body.value;
|
|
78
|
+
if (!field || !["skills", "needs", "offerings", "customNote"].includes(field)) {
|
|
79
|
+
sendError(res, "\u65E0\u6548\u5B57\u6BB5\uFF0C\u652F\u6301: skills, needs, offerings, customNote", 400);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const profile = await updateProfileField(
|
|
83
|
+
teamId,
|
|
84
|
+
auth.memberId,
|
|
85
|
+
field,
|
|
86
|
+
value
|
|
87
|
+
);
|
|
88
|
+
if (!profile) {
|
|
89
|
+
sendError(res, "Profile \u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u91C7\u96C6", 404);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
sendJSON(res, { profile });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (resource === "profiles" && method === "GET") {
|
|
96
|
+
const profiles = await loadAllProfiles(teamId);
|
|
97
|
+
sendJSON(res, { profiles });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (resource === "run" && method === "POST") {
|
|
101
|
+
try {
|
|
102
|
+
const round = await executeRound(teamId, auth.memberId, config);
|
|
103
|
+
sendJSON(res, { round });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
sendError(res, `\u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (resource === "rounds" && !subResource && method === "GET") {
|
|
110
|
+
const rounds = await listRounds(teamId);
|
|
111
|
+
const summaries = rounds.map((r) => ({
|
|
112
|
+
id: r.id,
|
|
113
|
+
status: r.status,
|
|
114
|
+
startedAt: r.startedAt,
|
|
115
|
+
completedAt: r.completedAt,
|
|
116
|
+
triggeredBy: r.triggeredBy,
|
|
117
|
+
matchCount: r.matches.length,
|
|
118
|
+
dialogueCount: r.dialogues.length,
|
|
119
|
+
digestCount: r.digests.length,
|
|
120
|
+
totalTokenUsage: r.totalTokenUsage,
|
|
121
|
+
error: r.error
|
|
122
|
+
}));
|
|
123
|
+
sendJSON(res, { rounds: summaries });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (resource === "rounds" && subResource && method === "GET") {
|
|
127
|
+
const round = await loadRound(teamId, subResource);
|
|
128
|
+
if (!round) {
|
|
129
|
+
sendError(res, "\u8F6E\u6B21\u4E0D\u5B58\u5728", 404);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
sendJSON(res, { round });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (resource === "digest" && method === "GET") {
|
|
136
|
+
const latestRound = await getLatestRound(teamId);
|
|
137
|
+
if (!latestRound) {
|
|
138
|
+
sendJSON(res, { digest: null, message: "\u6682\u65E0\u8F6E\u6B21\u8BB0\u5F55" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const myDigest = latestRound.digests.find((d) => d.memberId === auth.memberId);
|
|
142
|
+
sendJSON(res, { digest: myDigest || null, roundId: latestRound.id });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (resource === "config" && method === "GET") {
|
|
146
|
+
const netConfig = await loadNetworkConfig(teamId);
|
|
147
|
+
sendJSON(res, { config: netConfig });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (resource === "config" && method === "PUT") {
|
|
151
|
+
const body = await parseBody(req);
|
|
152
|
+
const existing = await loadNetworkConfig(teamId);
|
|
153
|
+
const updated = { ...existing, ...body };
|
|
154
|
+
await saveNetworkConfig(teamId, updated);
|
|
155
|
+
sendJSON(res, { config: updated });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
sendError(res, "Not Found", 404);
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
handleNetworkApiRequest
|
|
162
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearLocalToken,
|
|
3
|
+
generateId,
|
|
4
|
+
generateJoinCode,
|
|
5
|
+
generateToken,
|
|
6
|
+
isOwner,
|
|
7
|
+
loadLocalToken,
|
|
8
|
+
saveLocalToken,
|
|
9
|
+
validateToken
|
|
10
|
+
} from "./chunk-XDRCBMZZ.js";
|
|
11
|
+
import "./chunk-V5UHPPSY.js";
|
|
12
|
+
export {
|
|
13
|
+
clearLocalToken,
|
|
14
|
+
generateId,
|
|
15
|
+
generateJoinCode,
|
|
16
|
+
generateToken,
|
|
17
|
+
isOwner,
|
|
18
|
+
loadLocalToken,
|
|
19
|
+
saveLocalToken,
|
|
20
|
+
validateToken
|
|
21
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addItem,
|
|
3
|
+
archiveItem,
|
|
4
|
+
doneItem,
|
|
5
|
+
getItem,
|
|
6
|
+
getStats,
|
|
7
|
+
listItems,
|
|
8
|
+
pickItem,
|
|
9
|
+
reopenItem,
|
|
10
|
+
updateItem,
|
|
11
|
+
updateItemMetrics
|
|
12
|
+
} from "./chunk-MGLJ53QN.js";
|
|
13
|
+
export {
|
|
14
|
+
addItem,
|
|
15
|
+
archiveItem,
|
|
16
|
+
doneItem,
|
|
17
|
+
getItem,
|
|
18
|
+
getStats,
|
|
19
|
+
listItems,
|
|
20
|
+
pickItem,
|
|
21
|
+
reopenItem,
|
|
22
|
+
updateItem,
|
|
23
|
+
updateItemMetrics
|
|
24
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// src/admin/telemetry.ts
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { appendFile, stat, rename, mkdir } from "fs/promises";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
var HYPERCORE_DIR = join(process.env.HOME || "~", ".hypercore");
|
|
6
|
+
var TELEMETRY_DIR = join(HYPERCORE_DIR, "telemetry");
|
|
7
|
+
var EVENTS_FILE = join(TELEMETRY_DIR, "events.jsonl");
|
|
8
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
9
|
+
var enabled = true;
|
|
10
|
+
async function ensureDir() {
|
|
11
|
+
if (!existsSync(TELEMETRY_DIR)) {
|
|
12
|
+
await mkdir(TELEMETRY_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function rotateIfNeeded() {
|
|
16
|
+
try {
|
|
17
|
+
const s = await stat(EVENTS_FILE);
|
|
18
|
+
if (s.size > MAX_FILE_SIZE) {
|
|
19
|
+
const archivePath = join(TELEMETRY_DIR, "events.1.jsonl");
|
|
20
|
+
await rename(EVENTS_FILE, archivePath);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function trackEvent(event, data = {}) {
|
|
26
|
+
if (!enabled) return;
|
|
27
|
+
const entry = {
|
|
28
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29
|
+
event,
|
|
30
|
+
...data
|
|
31
|
+
};
|
|
32
|
+
(async () => {
|
|
33
|
+
try {
|
|
34
|
+
await ensureDir();
|
|
35
|
+
await rotateIfNeeded();
|
|
36
|
+
await appendFile(EVENTS_FILE, JSON.stringify(entry) + "\n", "utf-8");
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
}
|
|
41
|
+
function trackCmdExec(cmd, args, durationMs, ok, err) {
|
|
42
|
+
trackEvent("cmd_exec", { cmd, args, duration_ms: durationMs, ok, ...err ? { err } : {} });
|
|
43
|
+
}
|
|
44
|
+
function trackSessionStart(sessionId, model, provider) {
|
|
45
|
+
trackEvent("session_start", { session_id: sessionId, model, provider });
|
|
46
|
+
}
|
|
47
|
+
function trackSessionEnd(sessionId, rounds, tokensIn, tokensOut, durationMs) {
|
|
48
|
+
trackEvent("session_end", {
|
|
49
|
+
session_id: sessionId,
|
|
50
|
+
rounds,
|
|
51
|
+
tokens_in: tokensIn,
|
|
52
|
+
tokens_out: tokensOut,
|
|
53
|
+
duration_ms: durationMs
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function trackLineRun(line, topic, durationMs, ok, err) {
|
|
57
|
+
trackEvent("line_run", { line, topic, duration_ms: durationMs, ok, ...err ? { err } : {} });
|
|
58
|
+
}
|
|
59
|
+
function trackError(errorType, message, sourceFile, cmdContext) {
|
|
60
|
+
trackEvent("error", {
|
|
61
|
+
error_type: errorType,
|
|
62
|
+
message: message.slice(0, 200),
|
|
63
|
+
// 截断,不存长堆栈
|
|
64
|
+
source_file: sourceFile,
|
|
65
|
+
...cmdContext ? { cmd_context: cmdContext } : {}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function trackModelSwitch(fromModel, toModel, ok) {
|
|
69
|
+
trackEvent("model_switch", { from_model: fromModel, to_model: toModel, ok });
|
|
70
|
+
}
|
|
71
|
+
function trackWebStart(host, port, lanMode) {
|
|
72
|
+
trackEvent("web_start", { host, port, lan_mode: lanMode });
|
|
73
|
+
}
|
|
74
|
+
function trackTeamAction(action, teamId, memberCount) {
|
|
75
|
+
trackEvent("team_action", { action, ...teamId ? { team_id: teamId } : {}, ...memberCount != null ? { member_count: memberCount } : {} });
|
|
76
|
+
}
|
|
77
|
+
async function readEvents(days = 7) {
|
|
78
|
+
const events = [];
|
|
79
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
80
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
81
|
+
const cutoffStr = cutoff.toISOString();
|
|
82
|
+
for (const file of [EVENTS_FILE, join(TELEMETRY_DIR, "events.1.jsonl")]) {
|
|
83
|
+
if (!existsSync(file)) continue;
|
|
84
|
+
try {
|
|
85
|
+
const { readFile } = await import("fs/promises");
|
|
86
|
+
const content = await readFile(file, "utf-8");
|
|
87
|
+
for (const line of content.split("\n")) {
|
|
88
|
+
if (!line.trim()) continue;
|
|
89
|
+
try {
|
|
90
|
+
const evt = JSON.parse(line);
|
|
91
|
+
if (evt.ts >= cutoffStr) {
|
|
92
|
+
events.push(evt);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return events.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
101
|
+
}
|
|
102
|
+
async function getEventsSummary(days = 7) {
|
|
103
|
+
const events = await readEvents(days);
|
|
104
|
+
const eventCounts = {};
|
|
105
|
+
const cmdCounts = {};
|
|
106
|
+
let sessionCount = 0;
|
|
107
|
+
let totalRounds = 0;
|
|
108
|
+
const errorMap = {};
|
|
109
|
+
for (const evt of events) {
|
|
110
|
+
eventCounts[evt.event] = (eventCounts[evt.event] || 0) + 1;
|
|
111
|
+
if (evt.event === "cmd_exec") {
|
|
112
|
+
const cmd = String(evt.cmd || "unknown");
|
|
113
|
+
if (!cmdCounts[cmd]) cmdCounts[cmd] = { total: 0, ok: 0, fail: 0 };
|
|
114
|
+
cmdCounts[cmd].total++;
|
|
115
|
+
if (evt.ok) cmdCounts[cmd].ok++;
|
|
116
|
+
else cmdCounts[cmd].fail++;
|
|
117
|
+
}
|
|
118
|
+
if (evt.event === "session_end") {
|
|
119
|
+
sessionCount++;
|
|
120
|
+
totalRounds += evt.rounds || 0;
|
|
121
|
+
}
|
|
122
|
+
if (evt.event === "error") {
|
|
123
|
+
const errType = String(evt.error_type || "unknown");
|
|
124
|
+
errorMap[errType] = (errorMap[errType] || 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const errors = Object.entries(errorMap).map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count);
|
|
128
|
+
const now = /* @__PURE__ */ new Date();
|
|
129
|
+
const from = new Date(now);
|
|
130
|
+
from.setDate(from.getDate() - days);
|
|
131
|
+
return {
|
|
132
|
+
totalEvents: events.length,
|
|
133
|
+
eventCounts,
|
|
134
|
+
cmdCounts,
|
|
135
|
+
totalSessions: sessionCount,
|
|
136
|
+
avgSessionRounds: sessionCount > 0 ? Math.round(totalRounds / sessionCount * 10) / 10 : 0,
|
|
137
|
+
errors,
|
|
138
|
+
period: { from: from.toISOString().slice(0, 10), to: now.toISOString().slice(0, 10), days }
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function setTelemetryEnabled(value) {
|
|
142
|
+
enabled = value;
|
|
143
|
+
}
|
|
144
|
+
function getEventsFilePath() {
|
|
145
|
+
return EVENTS_FILE;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
trackEvent,
|
|
150
|
+
trackCmdExec,
|
|
151
|
+
trackSessionStart,
|
|
152
|
+
trackSessionEnd,
|
|
153
|
+
trackLineRun,
|
|
154
|
+
trackError,
|
|
155
|
+
trackModelSwitch,
|
|
156
|
+
trackWebStart,
|
|
157
|
+
trackTeamAction,
|
|
158
|
+
readEvents,
|
|
159
|
+
getEventsSummary,
|
|
160
|
+
setTelemetryEnabled,
|
|
161
|
+
getEventsFilePath
|
|
162
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadTeam,
|
|
3
|
+
updateMemberStatus
|
|
4
|
+
} from "./chunk-6OL3GA3P.js";
|
|
5
|
+
import {
|
|
6
|
+
validateToken
|
|
7
|
+
} from "./chunk-XDRCBMZZ.js";
|
|
8
|
+
|
|
9
|
+
// src/web/ws.ts
|
|
10
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
11
|
+
var clients = /* @__PURE__ */ new Map();
|
|
12
|
+
function getTeamClients(teamId) {
|
|
13
|
+
return Array.from(clients.values()).filter((c) => c.teamId === teamId);
|
|
14
|
+
}
|
|
15
|
+
function sendToMember(memberId, msg) {
|
|
16
|
+
const client = clients.get(memberId);
|
|
17
|
+
if (client && client.ws.readyState === WebSocket.OPEN) {
|
|
18
|
+
client.ws.send(JSON.stringify(msg));
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function broadcastToTeam(teamId, msg, excludeId) {
|
|
24
|
+
for (const client of getTeamClients(teamId)) {
|
|
25
|
+
if (client.member.id !== excludeId && client.ws.readyState === WebSocket.OPEN) {
|
|
26
|
+
client.ws.send(JSON.stringify(msg));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function broadcastToAll(teamId, msg) {
|
|
31
|
+
for (const client of getTeamClients(teamId)) {
|
|
32
|
+
if (client.ws.readyState === WebSocket.OPEN) {
|
|
33
|
+
client.ws.send(JSON.stringify(msg));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function makeMsg(type, payload, senderId) {
|
|
38
|
+
return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString(), senderId };
|
|
39
|
+
}
|
|
40
|
+
var pendingCheckpoints = /* @__PURE__ */ new Map();
|
|
41
|
+
function resolveCheckpoint(runId, stationIndex, response) {
|
|
42
|
+
const key = `${runId}:${stationIndex}`;
|
|
43
|
+
const resolver = pendingCheckpoints.get(key);
|
|
44
|
+
if (resolver) {
|
|
45
|
+
resolver(response);
|
|
46
|
+
pendingCheckpoints.delete(key);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
async function handleMessage(ws, raw) {
|
|
52
|
+
let msg;
|
|
53
|
+
try {
|
|
54
|
+
msg = JSON.parse(raw);
|
|
55
|
+
} catch {
|
|
56
|
+
ws.send(JSON.stringify(makeMsg("auth_fail", { reason: "\u65E0\u6548 JSON" })));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const existingClient = Array.from(clients.values()).find((c) => c.ws === ws);
|
|
60
|
+
if (!existingClient) {
|
|
61
|
+
if (msg.type !== "auth") {
|
|
62
|
+
ws.send(JSON.stringify(makeMsg("auth_fail", { reason: "\u8BF7\u5148\u8BA4\u8BC1" })));
|
|
63
|
+
ws.close();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const { teamId: teamId2, token } = msg.payload;
|
|
67
|
+
const team = await loadTeam(teamId2);
|
|
68
|
+
if (!team) {
|
|
69
|
+
ws.send(JSON.stringify(makeMsg("auth_fail", { reason: "\u56E2\u961F\u4E0D\u5B58\u5728" })));
|
|
70
|
+
ws.close();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const member2 = validateToken(team, token);
|
|
74
|
+
if (!member2) {
|
|
75
|
+
ws.send(JSON.stringify(makeMsg("auth_fail", { reason: "\u4EE4\u724C\u65E0\u6548" })));
|
|
76
|
+
ws.close();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
clients.set(member2.id, { ws, teamId: teamId2, member: member2 });
|
|
80
|
+
await updateMemberStatus(teamId2, member2.id, "online");
|
|
81
|
+
ws.send(JSON.stringify(makeMsg("auth_ok", {
|
|
82
|
+
memberId: member2.id,
|
|
83
|
+
memberName: member2.name,
|
|
84
|
+
role: member2.role,
|
|
85
|
+
teamName: team.name
|
|
86
|
+
})));
|
|
87
|
+
broadcastToTeam(teamId2, makeMsg("member_joined", {
|
|
88
|
+
memberId: member2.id,
|
|
89
|
+
memberName: member2.name
|
|
90
|
+
}), member2.id);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { teamId, member } = existingClient;
|
|
94
|
+
switch (msg.type) {
|
|
95
|
+
// ----- 任务事件 -----
|
|
96
|
+
case "task_created":
|
|
97
|
+
case "task_updated":
|
|
98
|
+
case "task_deleted":
|
|
99
|
+
broadcastToTeam(teamId, makeMsg(msg.type, msg.payload, member.id), member.id);
|
|
100
|
+
break;
|
|
101
|
+
// ----- 检查点响应 -----
|
|
102
|
+
case "run_checkpoint_response": {
|
|
103
|
+
const { runId, stationIndex, action, feedback } = msg.payload;
|
|
104
|
+
resolveCheckpoint(runId, stationIndex, { action, feedback });
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
// ----- 聊天消息 -----
|
|
108
|
+
case "chat_message": {
|
|
109
|
+
const { content } = msg.payload;
|
|
110
|
+
broadcastToAll(teamId, makeMsg("chat_message", {
|
|
111
|
+
senderId: member.id,
|
|
112
|
+
senderName: member.name,
|
|
113
|
+
content
|
|
114
|
+
}));
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "chat_typing": {
|
|
118
|
+
broadcastToTeam(teamId, makeMsg("chat_typing", {
|
|
119
|
+
memberId: member.id,
|
|
120
|
+
memberName: member.name
|
|
121
|
+
}), member.id);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
default:
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function attachWebSocket(_server, _config) {
|
|
129
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
130
|
+
wss.on("connection", (ws) => {
|
|
131
|
+
ws.on("message", (data) => {
|
|
132
|
+
handleMessage(ws, data.toString()).catch((err) => {
|
|
133
|
+
console.error("[WS] \u5904\u7406\u6D88\u606F\u9519\u8BEF:", err);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
ws.on("close", async () => {
|
|
137
|
+
for (const [memberId, client] of clients.entries()) {
|
|
138
|
+
if (client.ws === ws) {
|
|
139
|
+
clients.delete(memberId);
|
|
140
|
+
await updateMemberStatus(client.teamId, memberId, "offline");
|
|
141
|
+
broadcastToTeam(client.teamId, makeMsg("member_left", {
|
|
142
|
+
memberId,
|
|
143
|
+
memberName: client.member.name
|
|
144
|
+
}));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
ws.on("error", (err) => {
|
|
150
|
+
console.error("[WS] \u8FDE\u63A5\u9519\u8BEF:", err.message);
|
|
151
|
+
});
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
const isAuthed = Array.from(clients.values()).some((c) => c.ws === ws);
|
|
154
|
+
if (!isAuthed && ws.readyState === WebSocket.OPEN) {
|
|
155
|
+
ws.send(JSON.stringify(makeMsg("auth_fail", { reason: "\u8BA4\u8BC1\u8D85\u65F6" })));
|
|
156
|
+
ws.close();
|
|
157
|
+
}
|
|
158
|
+
}, 1e4);
|
|
159
|
+
});
|
|
160
|
+
return wss;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export {
|
|
164
|
+
sendToMember,
|
|
165
|
+
broadcastToTeam,
|
|
166
|
+
attachWebSocket
|
|
167
|
+
};
|