@pinixai/core 0.4.0 → 0.5.1
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/bindings.ts +8 -0
- package/src/cli.ts +4 -4
- package/src/clip.ts +10 -4
- package/src/http.ts +2 -2
- package/src/index.ts +3 -1
- package/src/ipc.ts +142 -12
- package/src/manifest.ts +70 -15
- package/src/mcp.ts +5 -2
package/package.json
CHANGED
package/src/bindings.ts
ADDED
package/src/cli.ts
CHANGED
|
@@ -93,9 +93,9 @@ function parseScalarValue(value: string, schema: ZodType): unknown {
|
|
|
93
93
|
const normalized = unwrapSchema(schema);
|
|
94
94
|
|
|
95
95
|
if (normalized instanceof z.ZodNumber) {
|
|
96
|
-
const parsed = Number
|
|
96
|
+
const parsed = Number(value);
|
|
97
97
|
|
|
98
|
-
if (Number.isNaN(parsed)) {
|
|
98
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
99
99
|
throw new Error(`Invalid number value: ${value}`);
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -111,9 +111,9 @@ function parseScalarValue(value: string, schema: ZodType): unknown {
|
|
|
111
111
|
const literalValue = literalValues[0];
|
|
112
112
|
|
|
113
113
|
if (typeof literalValue === "number") {
|
|
114
|
-
const parsed = Number
|
|
114
|
+
const parsed = Number(value);
|
|
115
115
|
|
|
116
|
-
if (Number.isNaN(parsed)) {
|
|
116
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
117
117
|
throw new Error(`Invalid number value: ${value}`);
|
|
118
118
|
}
|
|
119
119
|
|
package/src/clip.ts
CHANGED
|
@@ -20,9 +20,9 @@ function isHandlerDef(value: unknown): value is HandlerDef {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export abstract class Clip {
|
|
23
|
-
abstract name: string;
|
|
24
23
|
abstract domain: string;
|
|
25
24
|
abstract patterns: string[];
|
|
25
|
+
dependencies: Record<string, { package: string; version: string }> = {};
|
|
26
26
|
entities: Record<string, z.ZodObject<any>> = {};
|
|
27
27
|
|
|
28
28
|
protected readonly commands = new Map<string, HandlerDef>();
|
|
@@ -52,9 +52,9 @@ export abstract class Clip {
|
|
|
52
52
|
return serveHTTP(this);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const port = Number
|
|
55
|
+
const port = Number(portArg);
|
|
56
56
|
|
|
57
|
-
if (Number.isNaN(port)) {
|
|
57
|
+
if (Number.isNaN(port) || !Number.isFinite(port) || port !== Math.floor(port) || port < 0) {
|
|
58
58
|
console.error(`Invalid port: ${portArg}`);
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
@@ -101,8 +101,9 @@ export abstract class Clip {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
printHelp(): string {
|
|
104
|
+
const name = getClipName(this);
|
|
104
105
|
const lines: string[] = [
|
|
105
|
-
`${
|
|
106
|
+
name ? `${name} (${this.domain})` : this.domain,
|
|
106
107
|
"",
|
|
107
108
|
];
|
|
108
109
|
|
|
@@ -187,3 +188,8 @@ export abstract class Clip {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
|
|
192
|
+
export function getClipName(clip: Clip): string | undefined {
|
|
193
|
+
const value = (clip as { name?: unknown }).name;
|
|
194
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
195
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
|
-
import type
|
|
2
|
+
import { getClipName, type Clip } from "./clip";
|
|
3
3
|
import { zodToManifestType } from "./manifest";
|
|
4
4
|
|
|
5
5
|
const CORS_HEADERS = {
|
|
@@ -249,5 +249,5 @@ export async function serveHTTP(clip: Clip, port = 3000): Promise<void> {
|
|
|
249
249
|
fetch: (request) => handleRequest(clip, request),
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
console.error(`Clip "${clip.name}" running at http://localhost:${server.port}`);
|
|
252
|
+
console.error(`Clip "${getClipName(clip) ?? clip.constructor.name}" running at http://localhost:${server.port}`);
|
|
253
253
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { Clip } from "./clip";
|
|
2
|
+
export type { Binding, Bindings } from "./bindings";
|
|
2
3
|
export { command } from "./command";
|
|
3
4
|
export { handler, type HandlerDef, type Stream } from "./handler";
|
|
4
5
|
export { serveHTTP } from "./http";
|
|
5
|
-
export { serveIPC, invoke } from "./ipc";
|
|
6
|
+
export { serveIPC, invoke, redirectConsoleToStderr } from "./ipc";
|
|
7
|
+
export type { IPCCommandInfo, IPCManifest } from "./manifest";
|
|
6
8
|
export { serveMCP } from "./mcp";
|
|
7
9
|
export { z } from "zod";
|
package/src/ipc.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import type { Binding, Bindings } from "./bindings";
|
|
1
5
|
import type { Clip } from "./clip";
|
|
2
6
|
import type { Stream } from "./handler";
|
|
3
|
-
import { createIPCManifest } from "./manifest";
|
|
7
|
+
import { createIPCManifest, type IPCManifest } from "./manifest";
|
|
4
8
|
|
|
5
9
|
// === IPC Protocol Types ===
|
|
6
10
|
|
|
@@ -13,7 +17,12 @@ interface BaseMessage {
|
|
|
13
17
|
|
|
14
18
|
interface RegisterMessage extends BaseMessage {
|
|
15
19
|
type: "register";
|
|
16
|
-
manifest:
|
|
20
|
+
manifest: IPCManifest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RegisteredMessage extends BaseMessage {
|
|
24
|
+
type: "registered";
|
|
25
|
+
alias?: string;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
interface InvokeMessage extends BaseMessage {
|
|
@@ -22,6 +31,9 @@ interface InvokeMessage extends BaseMessage {
|
|
|
22
31
|
command?: string;
|
|
23
32
|
clip?: string;
|
|
24
33
|
input?: unknown;
|
|
34
|
+
hub?: string;
|
|
35
|
+
hub_token?: string;
|
|
36
|
+
clip_token?: string;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
interface ResultMessage extends BaseMessage {
|
|
@@ -36,10 +48,83 @@ interface ErrorMessage extends BaseMessage {
|
|
|
36
48
|
error: string;
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
interface ChunkMessage extends BaseMessage {
|
|
52
|
+
type: "chunk";
|
|
53
|
+
id: string;
|
|
54
|
+
output: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DoneMessage extends BaseMessage {
|
|
58
|
+
type: "done";
|
|
59
|
+
id: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
// === State ===
|
|
40
63
|
|
|
41
64
|
const pendingInvokes = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
42
65
|
let idCounter = 0;
|
|
66
|
+
let registeredAlias: string | undefined;
|
|
67
|
+
const bindings = loadBindings();
|
|
68
|
+
|
|
69
|
+
function loadBindings(): Bindings {
|
|
70
|
+
const bindingsPath = join(dirname(Bun.main), "bindings.json");
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(readFileSync(bindingsPath, "utf8")) as unknown;
|
|
74
|
+
|
|
75
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const loadedBindings: Bindings = {};
|
|
80
|
+
for (const [slot, value] of Object.entries(parsed)) {
|
|
81
|
+
const binding = normalizeBinding(value);
|
|
82
|
+
if (binding) {
|
|
83
|
+
loadedBindings[slot] = binding;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return loadedBindings;
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeBinding(value: unknown): Binding | null {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const candidate = value as Record<string, unknown>;
|
|
99
|
+
const alias = asNonEmptyString(candidate.alias);
|
|
100
|
+
|
|
101
|
+
if (!alias) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const binding: Binding = { alias };
|
|
106
|
+
const hub = asNonEmptyString(candidate.hub);
|
|
107
|
+
const hubToken = asNonEmptyString(candidate.hub_token);
|
|
108
|
+
const clipToken = asNonEmptyString(candidate.clip_token);
|
|
109
|
+
|
|
110
|
+
if (hub) {
|
|
111
|
+
binding.hub = hub;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (hubToken) {
|
|
115
|
+
binding.hub_token = hubToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (clipToken) {
|
|
119
|
+
binding.clip_token = clipToken;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return binding;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
126
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
127
|
+
}
|
|
43
128
|
|
|
44
129
|
function nextId(): string {
|
|
45
130
|
return `c${++idCounter}`;
|
|
@@ -51,22 +136,41 @@ function send(msg: Record<string, unknown>): void {
|
|
|
51
136
|
|
|
52
137
|
// === Public API ===
|
|
53
138
|
|
|
54
|
-
export async function invoke(
|
|
139
|
+
export async function invoke(slot: string, command: string, input: unknown): Promise<unknown> {
|
|
140
|
+
const binding = bindings[slot];
|
|
55
141
|
const id = nextId();
|
|
142
|
+
|
|
56
143
|
return new Promise((resolve, reject) => {
|
|
57
144
|
pendingInvokes.set(id, { resolve, reject });
|
|
58
|
-
send({
|
|
145
|
+
send({
|
|
146
|
+
id,
|
|
147
|
+
type: "invoke",
|
|
148
|
+
clip: binding?.alias ?? slot,
|
|
149
|
+
command,
|
|
150
|
+
input,
|
|
151
|
+
hub: binding?.hub,
|
|
152
|
+
hub_token: binding?.hub_token,
|
|
153
|
+
clip_token: binding?.clip_token,
|
|
154
|
+
});
|
|
59
155
|
});
|
|
60
156
|
}
|
|
61
157
|
|
|
158
|
+
// === Stdout Protection ===
|
|
159
|
+
|
|
160
|
+
export function redirectConsoleToStderr(): void {
|
|
161
|
+
const write = (...args: unknown[]) => {
|
|
162
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
163
|
+
};
|
|
164
|
+
console.log = write;
|
|
165
|
+
console.info = write;
|
|
166
|
+
console.debug = write;
|
|
167
|
+
}
|
|
168
|
+
|
|
62
169
|
// === IPC Server ===
|
|
63
170
|
|
|
64
171
|
export async function serveIPC(clip: Clip): Promise<void> {
|
|
65
172
|
// Redirect console.log to stderr so stdout is reserved for IPC
|
|
66
|
-
|
|
67
|
-
console.log = (...args: unknown[]) => {
|
|
68
|
-
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
69
|
-
};
|
|
173
|
+
redirectConsoleToStderr();
|
|
70
174
|
|
|
71
175
|
// Register with pinixd
|
|
72
176
|
const manifest = createIPCManifest(clip);
|
|
@@ -92,7 +196,7 @@ export async function serveIPC(clip: Clip): Promise<void> {
|
|
|
92
196
|
|
|
93
197
|
switch (msg.type) {
|
|
94
198
|
case "registered":
|
|
95
|
-
|
|
199
|
+
registeredAlias = asNonEmptyString((msg as RegisteredMessage).alias);
|
|
96
200
|
break;
|
|
97
201
|
|
|
98
202
|
case "invoke": {
|
|
@@ -121,10 +225,32 @@ export async function serveIPC(clip: Clip): Promise<void> {
|
|
|
121
225
|
break;
|
|
122
226
|
}
|
|
123
227
|
|
|
228
|
+
case "chunk": {
|
|
229
|
+
// Streaming chunks — currently ignored on client side
|
|
230
|
+
// Future: could accumulate or forward to a stream callback
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "done": {
|
|
235
|
+
const done = msg as DoneMessage;
|
|
236
|
+
const pending = pendingInvokes.get(done.id);
|
|
237
|
+
if (pending) {
|
|
238
|
+
pendingInvokes.delete(done.id);
|
|
239
|
+
pending.resolve(undefined);
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
124
244
|
default:
|
|
125
245
|
process.stderr.write(`[ipc] unknown message type: ${msg.type}\n`);
|
|
126
246
|
}
|
|
127
247
|
}
|
|
248
|
+
|
|
249
|
+
// Clean up pending invokes on EOF
|
|
250
|
+
for (const [id, pending] of pendingInvokes) {
|
|
251
|
+
pending.reject(new Error("IPC connection closed"));
|
|
252
|
+
}
|
|
253
|
+
pendingInvokes.clear();
|
|
128
254
|
}
|
|
129
255
|
|
|
130
256
|
async function handleInvoke(
|
|
@@ -138,7 +264,7 @@ async function handleInvoke(
|
|
|
138
264
|
}
|
|
139
265
|
|
|
140
266
|
try {
|
|
141
|
-
const parsed = cmd.input.
|
|
267
|
+
const parsed = await cmd.input.parseAsync(msg.input ?? {});
|
|
142
268
|
let streamed = false;
|
|
143
269
|
const stream: Stream = {
|
|
144
270
|
chunk(data: unknown): void {
|
|
@@ -154,7 +280,8 @@ async function handleInvoke(
|
|
|
154
280
|
return;
|
|
155
281
|
}
|
|
156
282
|
|
|
157
|
-
|
|
283
|
+
const validatedOutput = await cmd.output.parseAsync(output);
|
|
284
|
+
send({ id: msg.id, type: "result", output: validatedOutput });
|
|
158
285
|
} catch (err) {
|
|
159
286
|
send({ id: msg.id, type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
160
287
|
}
|
|
@@ -163,14 +290,17 @@ async function handleInvoke(
|
|
|
163
290
|
// === Line Reader ===
|
|
164
291
|
|
|
165
292
|
async function* createLineReader(stream: NodeJS.ReadableStream): AsyncGenerator<string> {
|
|
293
|
+
const decoder = new StringDecoder("utf8");
|
|
166
294
|
let buffer = "";
|
|
167
295
|
for await (const chunk of stream) {
|
|
168
|
-
buffer +=
|
|
296
|
+
buffer += decoder.write(chunk as Buffer);
|
|
169
297
|
const lines = buffer.split("\n");
|
|
170
298
|
buffer = lines.pop() ?? "";
|
|
171
299
|
for (const line of lines) {
|
|
172
300
|
if (line.trim()) yield line;
|
|
173
301
|
}
|
|
174
302
|
}
|
|
303
|
+
const remaining = decoder.end();
|
|
304
|
+
if (remaining) buffer += remaining;
|
|
175
305
|
if (buffer.trim()) yield buffer;
|
|
176
306
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
1
3
|
import { z, type ZodType } from "zod";
|
|
2
|
-
import type
|
|
4
|
+
import { getClipName, type Clip } from "./clip";
|
|
3
5
|
|
|
4
|
-
export interface
|
|
6
|
+
export interface IPCCommandInfo {
|
|
5
7
|
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
input?: string;
|
|
10
|
+
output?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IPCManifest {
|
|
6
14
|
domain: string;
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
description?: string;
|
|
16
|
+
commands: IPCCommandInfo[];
|
|
17
|
+
dependencies: Record<string, { package: string; version: string }>;
|
|
18
|
+
package?: string;
|
|
19
|
+
version?: string;
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
function formatLiteralValue(value: unknown): string {
|
|
@@ -94,11 +105,8 @@ export function zodToManifestType(schema: ZodType): string {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
export function generateManifest(clip: Clip): string {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
`Domain: ${clip.domain}`,
|
|
100
|
-
"",
|
|
101
|
-
];
|
|
108
|
+
const name = getClipName(clip);
|
|
109
|
+
const lines = name ? [`Clip: ${name}`, `Domain: ${clip.domain}`, ""] : [`Domain: ${clip.domain}`, ""];
|
|
102
110
|
|
|
103
111
|
if (clip.patterns.length > 0) {
|
|
104
112
|
lines.push("Patterns:");
|
|
@@ -147,15 +155,62 @@ export function generateManifest(clip: Clip): string {
|
|
|
147
155
|
return lines.join("\n");
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
function findJsonFile(filename: string): Record<string, unknown> | null {
|
|
159
|
+
let dir = dirname(Bun.main);
|
|
160
|
+
for (;;) {
|
|
161
|
+
const filePath = join(dir, filename);
|
|
162
|
+
if (existsSync(filePath)) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const parent = dirname(dir);
|
|
170
|
+
if (parent === dir) break;
|
|
171
|
+
dir = parent;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolvePackageInfo(): { package?: string; version?: string } {
|
|
177
|
+
const pinixJson = findJsonFile("pinix.json");
|
|
178
|
+
const packageJson = findJsonFile("package.json");
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
package: asString(pinixJson?.name) ?? asString(packageJson?.name),
|
|
182
|
+
version: asString(pinixJson?.version) ?? asString(packageJson?.version),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function asString(value: unknown): string | undefined {
|
|
187
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
150
190
|
export function createIPCManifest(clip: Clip): IPCManifest {
|
|
151
|
-
const
|
|
191
|
+
const pkgInfo = resolvePackageInfo();
|
|
192
|
+
|
|
193
|
+
const commands: IPCCommandInfo[] = [];
|
|
194
|
+
|
|
195
|
+
for (const [name, handler] of clip.getCommands()) {
|
|
196
|
+
const description = clip.getCommandDescription(name);
|
|
197
|
+
const cmd: IPCCommandInfo = { name };
|
|
198
|
+
if (description) cmd.description = description;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
cmd.input = JSON.stringify(z.toJSONSchema(handler.input));
|
|
202
|
+
} catch {}
|
|
203
|
+
try {
|
|
204
|
+
cmd.output = JSON.stringify(z.toJSONSchema(handler.output));
|
|
205
|
+
} catch {}
|
|
206
|
+
|
|
207
|
+
commands.push(cmd);
|
|
208
|
+
}
|
|
152
209
|
|
|
153
210
|
return {
|
|
154
|
-
name: clip.name,
|
|
155
211
|
domain: clip.domain,
|
|
156
|
-
commands
|
|
157
|
-
dependencies:
|
|
158
|
-
|
|
159
|
-
: [],
|
|
212
|
+
commands,
|
|
213
|
+
dependencies: clip.dependencies,
|
|
214
|
+
...pkgInfo,
|
|
160
215
|
};
|
|
161
216
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import type
|
|
3
|
+
import { getClipName, type Clip } from "./clip";
|
|
4
|
+
import { redirectConsoleToStderr } from "./ipc";
|
|
4
5
|
|
|
5
6
|
export async function serveMCP(clip: Clip): Promise<void> {
|
|
7
|
+
// Redirect console.log to stderr so stdout is reserved for MCP JSON-RPC
|
|
8
|
+
redirectConsoleToStderr();
|
|
6
9
|
const server = new McpServer({
|
|
7
|
-
name: clip.name,
|
|
10
|
+
name: getClipName(clip) ?? clip.constructor.name,
|
|
8
11
|
version: "1.0.0",
|
|
9
12
|
});
|
|
10
13
|
|