@pinixai/core 0.2.0 → 0.3.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/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/ipc.ts +123 -122
- package/src/manifest.ts +20 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { Clip } from "./clip";
|
|
|
2
2
|
export { command } from "./command";
|
|
3
3
|
export { handler, type HandlerDef } from "./handler";
|
|
4
4
|
export { serveHTTP } from "./http";
|
|
5
|
-
export { serveIPC } from "./ipc";
|
|
5
|
+
export { serveIPC, invoke } from "./ipc";
|
|
6
6
|
export { serveMCP } from "./mcp";
|
|
7
7
|
export { z } from "zod";
|
package/src/ipc.ts
CHANGED
|
@@ -1,160 +1,161 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
1
|
import type { Clip } from "./clip";
|
|
2
|
+
import { createIPCManifest } from "./manifest";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
id?: unknown;
|
|
6
|
-
command?: unknown;
|
|
7
|
-
input?: unknown;
|
|
8
|
-
};
|
|
4
|
+
// === IPC Protocol Types ===
|
|
9
5
|
|
|
10
|
-
type
|
|
11
|
-
| {
|
|
12
|
-
id: unknown;
|
|
13
|
-
output: unknown;
|
|
14
|
-
}
|
|
15
|
-
| {
|
|
16
|
-
id: unknown;
|
|
17
|
-
error: {
|
|
18
|
-
message: string;
|
|
19
|
-
code: string;
|
|
20
|
-
};
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function toErrorMessage(error: unknown): string {
|
|
24
|
-
return error instanceof Error ? error.message : String(error);
|
|
25
|
-
}
|
|
6
|
+
type MessageType = "register" | "registered" | "invoke" | "result" | "error" | "chunk" | "done";
|
|
26
7
|
|
|
27
|
-
|
|
28
|
-
|
|
8
|
+
interface BaseMessage {
|
|
9
|
+
type: MessageType;
|
|
10
|
+
id?: string;
|
|
29
11
|
}
|
|
30
12
|
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
interface RegisterMessage extends BaseMessage {
|
|
14
|
+
type: "register";
|
|
15
|
+
manifest: { name: string; domain: string; commands: string[]; dependencies: string[] };
|
|
33
16
|
}
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
};
|
|
18
|
+
interface InvokeMessage extends BaseMessage {
|
|
19
|
+
type: "invoke";
|
|
20
|
+
id: string;
|
|
21
|
+
command?: string;
|
|
22
|
+
clip?: string;
|
|
23
|
+
input?: unknown;
|
|
43
24
|
}
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
interface ResultMessage extends BaseMessage {
|
|
27
|
+
type: "result";
|
|
28
|
+
id: string;
|
|
29
|
+
output: unknown;
|
|
30
|
+
}
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
32
|
+
interface ErrorMessage extends BaseMessage {
|
|
33
|
+
type: "error";
|
|
34
|
+
id: string;
|
|
35
|
+
error: string;
|
|
36
|
+
}
|
|
54
37
|
|
|
55
|
-
|
|
38
|
+
// === State ===
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
40
|
+
const pendingInvokes = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
41
|
+
let idCounter = 0;
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
function nextId(): string {
|
|
44
|
+
return `c${++idCounter}`;
|
|
45
|
+
}
|
|
63
46
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
47
|
+
function send(msg: Record<string, unknown>): void {
|
|
48
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
49
|
+
}
|
|
68
50
|
|
|
69
|
-
|
|
70
|
-
writeResponse(createErrorResponse(id, "Request input must be an object", "INVALID_INPUT"));
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
51
|
+
// === Public API ===
|
|
73
52
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
id,
|
|
81
|
-
output: parsedOutput,
|
|
82
|
-
});
|
|
83
|
-
} catch (error) {
|
|
84
|
-
writeResponse(createErrorResponse(id, toErrorMessage(error), "COMMAND_ERROR"));
|
|
85
|
-
}
|
|
53
|
+
export async function invoke(clip: string, command: string, input: unknown): Promise<unknown> {
|
|
54
|
+
const id = nextId();
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
pendingInvokes.set(id, { resolve, reject });
|
|
57
|
+
send({ id, type: "invoke", clip, command, input });
|
|
58
|
+
});
|
|
86
59
|
}
|
|
87
60
|
|
|
88
|
-
|
|
89
|
-
console.log = (...args: unknown[]) => {
|
|
90
|
-
const serialized = args
|
|
91
|
-
.map((arg) => {
|
|
92
|
-
if (typeof arg === "string") {
|
|
93
|
-
return arg;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
return JSON.stringify(arg);
|
|
98
|
-
} catch {
|
|
99
|
-
return String(arg);
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
.join(" ");
|
|
61
|
+
// === IPC Server ===
|
|
103
62
|
|
|
104
|
-
|
|
63
|
+
export async function serveIPC(clip: Clip): Promise<void> {
|
|
64
|
+
// Redirect console.log to stderr so stdout is reserved for IPC
|
|
65
|
+
const origLog = console.log;
|
|
66
|
+
console.log = (...args: unknown[]) => {
|
|
67
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
105
68
|
};
|
|
106
|
-
}
|
|
107
69
|
|
|
108
|
-
|
|
109
|
-
|
|
70
|
+
// Register with pinixd
|
|
71
|
+
const manifest = createIPCManifest(clip);
|
|
72
|
+
send({ type: "register", manifest });
|
|
73
|
+
|
|
74
|
+
// Read messages from stdin
|
|
75
|
+
const reader = createLineReader(process.stdin);
|
|
76
|
+
const commands = clip.getCommands();
|
|
77
|
+
|
|
78
|
+
for await (const line of reader) {
|
|
79
|
+
let msg: BaseMessage;
|
|
80
|
+
try {
|
|
81
|
+
msg = JSON.parse(line) as BaseMessage;
|
|
82
|
+
} catch {
|
|
83
|
+
process.stderr.write(`[ipc] invalid JSON: ${line}\n`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
110
86
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
87
|
+
if (!msg.type) {
|
|
88
|
+
process.stderr.write(`[ipc] message missing type: ${line}\n`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
114
91
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
92
|
+
switch (msg.type) {
|
|
93
|
+
case "registered":
|
|
94
|
+
// Registration confirmed
|
|
95
|
+
break;
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
case "invoke": {
|
|
98
|
+
const inv = msg as InvokeMessage;
|
|
99
|
+
handleInvoke(inv, commands);
|
|
120
100
|
break;
|
|
121
101
|
}
|
|
122
102
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const line = buffer.slice(0, newlineIndex).trim();
|
|
133
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
134
|
-
|
|
135
|
-
if (line.length === 0) {
|
|
136
|
-
continue;
|
|
103
|
+
case "result": {
|
|
104
|
+
const res = msg as ResultMessage;
|
|
105
|
+
const pending = pendingInvokes.get(res.id);
|
|
106
|
+
if (pending) {
|
|
107
|
+
pendingInvokes.delete(res.id);
|
|
108
|
+
pending.resolve(res.output);
|
|
137
109
|
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
138
112
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
113
|
+
case "error": {
|
|
114
|
+
const err = msg as ErrorMessage;
|
|
115
|
+
const pending = pendingInvokes.get(err.id);
|
|
116
|
+
if (pending) {
|
|
117
|
+
pendingInvokes.delete(err.id);
|
|
118
|
+
pending.reject(new Error(err.error));
|
|
143
119
|
}
|
|
120
|
+
break;
|
|
144
121
|
}
|
|
122
|
+
|
|
123
|
+
default:
|
|
124
|
+
process.stderr.write(`[ipc] unknown message type: ${msg.type}\n`);
|
|
145
125
|
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
146
128
|
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
async function handleInvoke(
|
|
130
|
+
msg: InvokeMessage,
|
|
131
|
+
commands: ReturnType<Clip["getCommands"]>,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const cmd = commands.get(msg.command ?? "");
|
|
134
|
+
if (!cmd) {
|
|
135
|
+
send({ id: msg.id, type: "error", error: `unknown command: ${msg.command}` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
139
|
+
try {
|
|
140
|
+
const parsed = cmd.input.parse(msg.input ?? {});
|
|
141
|
+
const output = await cmd.fn(parsed);
|
|
142
|
+
send({ id: msg.id, type: "result", output });
|
|
143
|
+
} catch (err) {
|
|
144
|
+
send({ id: msg.id, type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// === Line Reader ===
|
|
149
|
+
|
|
150
|
+
async function* createLineReader(stream: NodeJS.ReadableStream): AsyncGenerator<string> {
|
|
151
|
+
let buffer = "";
|
|
152
|
+
for await (const chunk of stream) {
|
|
153
|
+
buffer += chunk.toString();
|
|
154
|
+
const lines = buffer.split("\n");
|
|
155
|
+
buffer = lines.pop() ?? "";
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (line.trim()) yield line;
|
|
156
158
|
}
|
|
157
|
-
} finally {
|
|
158
|
-
reader.releaseLock();
|
|
159
159
|
}
|
|
160
|
+
if (buffer.trim()) yield buffer;
|
|
160
161
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { z, type ZodType } from "zod";
|
|
2
2
|
import type { Clip } from "./clip";
|
|
3
3
|
|
|
4
|
+
export interface IPCManifest {
|
|
5
|
+
name: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
commands: string[];
|
|
8
|
+
dependencies: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
function formatLiteralValue(value: unknown): string {
|
|
5
12
|
return JSON.stringify(value);
|
|
6
13
|
}
|
|
@@ -139,3 +146,16 @@ export function generateManifest(clip: Clip): string {
|
|
|
139
146
|
|
|
140
147
|
return lines.join("\n");
|
|
141
148
|
}
|
|
149
|
+
|
|
150
|
+
export function createIPCManifest(clip: Clip): IPCManifest {
|
|
151
|
+
const dependencies = (clip as Clip & { dependencies?: unknown }).dependencies;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: clip.name,
|
|
155
|
+
domain: clip.domain,
|
|
156
|
+
commands: Array.from(clip.getCommands().keys()),
|
|
157
|
+
dependencies: Array.isArray(dependencies)
|
|
158
|
+
? dependencies.filter((dependency): dependency is string => typeof dependency === "string")
|
|
159
|
+
: [],
|
|
160
|
+
};
|
|
161
|
+
}
|