@cortexkit/opencode-magic-context 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/dist/cli.js +4 -3
- package/dist/config/index.d.ts +3 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29591 -60964
- package/dist/plugin/rpc-handlers.d.ts +11 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -0
- package/dist/shared/conflict-detector.d.ts +0 -4
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +16 -0
- package/dist/shared/rpc-client.d.ts.map +1 -0
- package/dist/shared/rpc-notifications.d.ts +21 -0
- package/dist/shared/rpc-notifications.d.ts.map +1 -0
- package/dist/shared/rpc-server.d.ts +17 -0
- package/dist/shared/rpc-server.d.ts.map +1 -0
- package/dist/shared/rpc-types.d.ts +59 -0
- package/dist/shared/rpc-types.d.ts.map +1 -0
- package/dist/shared/rpc-utils.d.ts +8 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -0
- package/dist/tui/data/context-db.d.ts +17 -69
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/shared/conflict-detector.ts +1 -17
- package/src/shared/logger.ts +6 -1
- package/src/shared/rpc-client.ts +123 -0
- package/src/shared/rpc-notifications.ts +44 -0
- package/src/shared/rpc-server.ts +136 -0
- package/src/shared/rpc-types.ts +58 -0
- package/src/shared/rpc-utils.ts +16 -0
- package/src/tui/data/context-db.ts +99 -625
- package/src/tui/index.tsx +53 -55
- package/src/tui/slots/sidebar-content.tsx +8 -7
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { log } from "./logger";
|
|
5
|
+
import { rpcPortFilePath } from "./rpc-utils";
|
|
6
|
+
|
|
7
|
+
type RpcHandler = (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
8
|
+
|
|
9
|
+
export class MagicContextRpcServer {
|
|
10
|
+
private server: Server | null = null;
|
|
11
|
+
private port = 0;
|
|
12
|
+
private handlers = new Map<string, RpcHandler>();
|
|
13
|
+
private portFilePath: string;
|
|
14
|
+
|
|
15
|
+
constructor(storageDir: string, directory: string) {
|
|
16
|
+
this.portFilePath = rpcPortFilePath(storageDir, directory);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Register an RPC method handler. */
|
|
20
|
+
handle(method: string, handler: RpcHandler): void {
|
|
21
|
+
this.handlers.set(method, handler);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Start the server on a random port, write port to disk. */
|
|
25
|
+
async start(): Promise<number> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const server = createServer((req, res) => this.dispatch(req, res));
|
|
28
|
+
|
|
29
|
+
server.on("error", (err) => {
|
|
30
|
+
log(`[rpc] server error: ${err.message}`);
|
|
31
|
+
reject(err);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
server.listen(0, "127.0.0.1", () => {
|
|
35
|
+
const addr = server.address();
|
|
36
|
+
if (!addr || typeof addr === "string") {
|
|
37
|
+
reject(new Error("Failed to get server address"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.port = addr.port;
|
|
41
|
+
this.server = server;
|
|
42
|
+
|
|
43
|
+
// Write port file atomically
|
|
44
|
+
try {
|
|
45
|
+
const dir = dirname(this.portFilePath);
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
const tmpPath = `${this.portFilePath}.tmp`;
|
|
48
|
+
writeFileSync(tmpPath, String(this.port), "utf-8");
|
|
49
|
+
renameSync(tmpPath, this.portFilePath);
|
|
50
|
+
log(`[rpc] server listening on 127.0.0.1:${this.port}`);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
log(`[rpc] failed to write port file: ${err}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
resolve(this.port);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Don't keep the process alive just for the RPC server
|
|
59
|
+
server.unref();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Stop the server and clean up port file. */
|
|
64
|
+
stop(): void {
|
|
65
|
+
if (this.server) {
|
|
66
|
+
this.server.close();
|
|
67
|
+
this.server = null;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(this.portFilePath);
|
|
71
|
+
} catch {
|
|
72
|
+
// Intentional: port file may already be gone
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private dispatch(req: IncomingMessage, res: ServerResponse): void {
|
|
77
|
+
const url = req.url ?? "";
|
|
78
|
+
|
|
79
|
+
// CORS headers for same-origin fetch
|
|
80
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
81
|
+
|
|
82
|
+
if (req.method === "GET" && url === "/health") {
|
|
83
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
84
|
+
res.end(JSON.stringify({ ok: true, pid: process.pid }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (req.method !== "POST" || !url.startsWith("/rpc/")) {
|
|
89
|
+
res.writeHead(404);
|
|
90
|
+
res.end("Not Found");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const method = url.slice(5); // strip "/rpc/"
|
|
95
|
+
const handler = this.handlers.get(method);
|
|
96
|
+
if (!handler) {
|
|
97
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
98
|
+
res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let body = "";
|
|
103
|
+
req.on("data", (chunk: Buffer) => {
|
|
104
|
+
body += chunk.toString();
|
|
105
|
+
if (body.length > 1_048_576) {
|
|
106
|
+
res.writeHead(413);
|
|
107
|
+
res.end("Request too large");
|
|
108
|
+
req.destroy();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
req.on("end", () => {
|
|
113
|
+
let params: Record<string, unknown> = {};
|
|
114
|
+
try {
|
|
115
|
+
if (body.length > 0) {
|
|
116
|
+
params = JSON.parse(body);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
120
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
handler(params)
|
|
125
|
+
.then((result) => {
|
|
126
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
127
|
+
res.end(JSON.stringify(result));
|
|
128
|
+
})
|
|
129
|
+
.catch((err) => {
|
|
130
|
+
log(`[rpc] handler error: ${method} => ${err}`);
|
|
131
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
132
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for RPC between server and TUI plugins.
|
|
3
|
+
* Both sides import these — no SQLite dependency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SidebarSnapshot {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
usagePercentage: number;
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
systemPromptTokens: number;
|
|
11
|
+
compartmentCount: number;
|
|
12
|
+
factCount: number;
|
|
13
|
+
memoryCount: number;
|
|
14
|
+
memoryBlockCount: number;
|
|
15
|
+
pendingOpsCount: number;
|
|
16
|
+
historianRunning: boolean;
|
|
17
|
+
compartmentInProgress: boolean;
|
|
18
|
+
sessionNoteCount: number;
|
|
19
|
+
readySmartNoteCount: number;
|
|
20
|
+
cacheTtl: string;
|
|
21
|
+
lastDreamerRunAt: number | null;
|
|
22
|
+
projectIdentity: string | null;
|
|
23
|
+
compartmentTokens: number;
|
|
24
|
+
factTokens: number;
|
|
25
|
+
memoryTokens: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StatusDetail extends SidebarSnapshot {
|
|
29
|
+
tagCounter: number;
|
|
30
|
+
activeTags: number;
|
|
31
|
+
droppedTags: number;
|
|
32
|
+
totalTags: number;
|
|
33
|
+
activeBytes: number;
|
|
34
|
+
lastResponseTime: number;
|
|
35
|
+
lastNudgeTokens: number;
|
|
36
|
+
lastNudgeBand: string;
|
|
37
|
+
lastTransformError: string | null;
|
|
38
|
+
isSubagent: boolean;
|
|
39
|
+
pendingOps: Array<{ tagId: number; operation: string }>;
|
|
40
|
+
contextLimit: number;
|
|
41
|
+
cacheTtlMs: number;
|
|
42
|
+
cacheRemainingMs: number;
|
|
43
|
+
cacheExpired: boolean;
|
|
44
|
+
executeThreshold: number;
|
|
45
|
+
protectedTagCount: number;
|
|
46
|
+
nudgeInterval: number;
|
|
47
|
+
historyBudgetPercentage: number;
|
|
48
|
+
nextNudgeAfter: number;
|
|
49
|
+
historyBlockTokens: number;
|
|
50
|
+
compressionBudget: number | null;
|
|
51
|
+
compressionUsage: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface RpcNotificationMessage {
|
|
55
|
+
type: string;
|
|
56
|
+
payload: Record<string, unknown>;
|
|
57
|
+
sessionId?: string;
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stable hash for a project directory — scopes RPC port files per-project
|
|
6
|
+
* so multiple OpenCode instances don't collide.
|
|
7
|
+
*/
|
|
8
|
+
export function projectHash(directory: string): string {
|
|
9
|
+
const normalized = directory.replace(/\/+$/, "");
|
|
10
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Per-project RPC port file path. */
|
|
14
|
+
export function rpcPortFilePath(storageDir: string, directory: string): string {
|
|
15
|
+
return join(storageDir, "rpc", projectHash(directory), "port");
|
|
16
|
+
}
|