@pinixai/core 0.3.0 → 0.5.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/bindings.ts +8 -0
- package/src/cli.ts +4 -4
- package/src/clip.ts +10 -4
- package/src/handler.ts +6 -2
- package/src/http.ts +2 -2
- package/src/index.ts +4 -2
- package/src/ipc.ts +158 -13
- package/src/manifest.ts +44 -13
- 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/handler.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { z, type ZodType } from "zod";
|
|
2
2
|
|
|
3
|
+
export interface Stream {
|
|
4
|
+
chunk(data: unknown): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export interface HandlerDef<I extends ZodType = ZodType, O extends ZodType = ZodType> {
|
|
4
8
|
input: I;
|
|
5
9
|
output: O;
|
|
6
|
-
fn: (input: z.infer<I
|
|
10
|
+
fn: (input: z.infer<I>, stream?: Stream) => Promise<z.infer<O>>;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export function handler<I extends ZodType, O extends ZodType>(
|
|
10
14
|
input: I,
|
|
11
15
|
output: O,
|
|
12
|
-
fn: (input: z.infer<I
|
|
16
|
+
fn: (input: z.infer<I>, stream?: Stream) => Promise<z.infer<O>>,
|
|
13
17
|
): HandlerDef<I, O> {
|
|
14
18
|
return { input, output, fn };
|
|
15
19
|
}
|
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
|
-
export { handler, type HandlerDef } from "./handler";
|
|
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 { IPCManifest } from "./manifest";
|
|
6
8
|
export { serveMCP } from "./mcp";
|
|
7
9
|
export { z } from "zod";
|
package/src/ipc.ts
CHANGED
|
@@ -1,5 +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
|
-
import {
|
|
6
|
+
import type { Stream } from "./handler";
|
|
7
|
+
import { createIPCManifest, type IPCManifest } from "./manifest";
|
|
3
8
|
|
|
4
9
|
// === IPC Protocol Types ===
|
|
5
10
|
|
|
@@ -12,7 +17,12 @@ interface BaseMessage {
|
|
|
12
17
|
|
|
13
18
|
interface RegisterMessage extends BaseMessage {
|
|
14
19
|
type: "register";
|
|
15
|
-
manifest:
|
|
20
|
+
manifest: IPCManifest;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RegisteredMessage extends BaseMessage {
|
|
24
|
+
type: "registered";
|
|
25
|
+
alias?: string;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
interface InvokeMessage extends BaseMessage {
|
|
@@ -21,6 +31,9 @@ interface InvokeMessage extends BaseMessage {
|
|
|
21
31
|
command?: string;
|
|
22
32
|
clip?: string;
|
|
23
33
|
input?: unknown;
|
|
34
|
+
hub?: string;
|
|
35
|
+
hub_token?: string;
|
|
36
|
+
clip_token?: string;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
interface ResultMessage extends BaseMessage {
|
|
@@ -35,10 +48,83 @@ interface ErrorMessage extends BaseMessage {
|
|
|
35
48
|
error: string;
|
|
36
49
|
}
|
|
37
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
|
+
|
|
38
62
|
// === State ===
|
|
39
63
|
|
|
40
64
|
const pendingInvokes = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
41
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
|
+
}
|
|
42
128
|
|
|
43
129
|
function nextId(): string {
|
|
44
130
|
return `c${++idCounter}`;
|
|
@@ -50,22 +136,41 @@ function send(msg: Record<string, unknown>): void {
|
|
|
50
136
|
|
|
51
137
|
// === Public API ===
|
|
52
138
|
|
|
53
|
-
export async function invoke(
|
|
139
|
+
export async function invoke(slot: string, command: string, input: unknown): Promise<unknown> {
|
|
140
|
+
const binding = bindings[slot];
|
|
54
141
|
const id = nextId();
|
|
142
|
+
|
|
55
143
|
return new Promise((resolve, reject) => {
|
|
56
144
|
pendingInvokes.set(id, { resolve, reject });
|
|
57
|
-
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
|
+
});
|
|
58
155
|
});
|
|
59
156
|
}
|
|
60
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
|
+
|
|
61
169
|
// === IPC Server ===
|
|
62
170
|
|
|
63
171
|
export async function serveIPC(clip: Clip): Promise<void> {
|
|
64
172
|
// Redirect console.log to stderr so stdout is reserved for IPC
|
|
65
|
-
|
|
66
|
-
console.log = (...args: unknown[]) => {
|
|
67
|
-
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
68
|
-
};
|
|
173
|
+
redirectConsoleToStderr();
|
|
69
174
|
|
|
70
175
|
// Register with pinixd
|
|
71
176
|
const manifest = createIPCManifest(clip);
|
|
@@ -91,7 +196,7 @@ export async function serveIPC(clip: Clip): Promise<void> {
|
|
|
91
196
|
|
|
92
197
|
switch (msg.type) {
|
|
93
198
|
case "registered":
|
|
94
|
-
|
|
199
|
+
registeredAlias = asNonEmptyString((msg as RegisteredMessage).alias);
|
|
95
200
|
break;
|
|
96
201
|
|
|
97
202
|
case "invoke": {
|
|
@@ -120,10 +225,32 @@ export async function serveIPC(clip: Clip): Promise<void> {
|
|
|
120
225
|
break;
|
|
121
226
|
}
|
|
122
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
|
+
|
|
123
244
|
default:
|
|
124
245
|
process.stderr.write(`[ipc] unknown message type: ${msg.type}\n`);
|
|
125
246
|
}
|
|
126
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();
|
|
127
254
|
}
|
|
128
255
|
|
|
129
256
|
async function handleInvoke(
|
|
@@ -137,9 +264,24 @@ async function handleInvoke(
|
|
|
137
264
|
}
|
|
138
265
|
|
|
139
266
|
try {
|
|
140
|
-
const parsed = cmd.input.
|
|
141
|
-
|
|
142
|
-
|
|
267
|
+
const parsed = await cmd.input.parseAsync(msg.input ?? {});
|
|
268
|
+
let streamed = false;
|
|
269
|
+
const stream: Stream = {
|
|
270
|
+
chunk(data: unknown): void {
|
|
271
|
+
streamed = true;
|
|
272
|
+
send({ id: msg.id, type: "chunk", output: data });
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const output = await cmd.fn(parsed, stream);
|
|
277
|
+
|
|
278
|
+
if (streamed) {
|
|
279
|
+
send({ id: msg.id, type: "done" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const validatedOutput = await cmd.output.parseAsync(output);
|
|
284
|
+
send({ id: msg.id, type: "result", output: validatedOutput });
|
|
143
285
|
} catch (err) {
|
|
144
286
|
send({ id: msg.id, type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
145
287
|
}
|
|
@@ -148,14 +290,17 @@ async function handleInvoke(
|
|
|
148
290
|
// === Line Reader ===
|
|
149
291
|
|
|
150
292
|
async function* createLineReader(stream: NodeJS.ReadableStream): AsyncGenerator<string> {
|
|
293
|
+
const decoder = new StringDecoder("utf8");
|
|
151
294
|
let buffer = "";
|
|
152
295
|
for await (const chunk of stream) {
|
|
153
|
-
buffer +=
|
|
296
|
+
buffer += decoder.write(chunk as Buffer);
|
|
154
297
|
const lines = buffer.split("\n");
|
|
155
298
|
buffer = lines.pop() ?? "";
|
|
156
299
|
for (const line of lines) {
|
|
157
300
|
if (line.trim()) yield line;
|
|
158
301
|
}
|
|
159
302
|
}
|
|
303
|
+
const remaining = decoder.end();
|
|
304
|
+
if (remaining) buffer += remaining;
|
|
160
305
|
if (buffer.trim()) yield buffer;
|
|
161
306
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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
6
|
export interface IPCManifest {
|
|
5
|
-
name: string;
|
|
6
7
|
domain: string;
|
|
8
|
+
description?: string;
|
|
7
9
|
commands: string[];
|
|
8
|
-
dependencies: string
|
|
10
|
+
dependencies: Record<string, { package: string; version: string }>;
|
|
11
|
+
package?: string;
|
|
12
|
+
version?: string;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
function formatLiteralValue(value: unknown): string {
|
|
@@ -94,11 +98,8 @@ export function zodToManifestType(schema: ZodType): string {
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
export function generateManifest(clip: Clip): string {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
`Domain: ${clip.domain}`,
|
|
100
|
-
"",
|
|
101
|
-
];
|
|
101
|
+
const name = getClipName(clip);
|
|
102
|
+
const lines = name ? [`Clip: ${name}`, `Domain: ${clip.domain}`, ""] : [`Domain: ${clip.domain}`, ""];
|
|
102
103
|
|
|
103
104
|
if (clip.patterns.length > 0) {
|
|
104
105
|
lines.push("Patterns:");
|
|
@@ -147,15 +148,45 @@ export function generateManifest(clip: Clip): string {
|
|
|
147
148
|
return lines.join("\n");
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
function findJsonFile(filename: string): Record<string, unknown> | null {
|
|
152
|
+
let dir = dirname(Bun.main);
|
|
153
|
+
for (;;) {
|
|
154
|
+
const filePath = join(dir, filename);
|
|
155
|
+
if (existsSync(filePath)) {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const parent = dirname(dir);
|
|
163
|
+
if (parent === dir) break;
|
|
164
|
+
dir = parent;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolvePackageInfo(): { package?: string; version?: string } {
|
|
170
|
+
const pinixJson = findJsonFile("pinix.json");
|
|
171
|
+
const packageJson = findJsonFile("package.json");
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
package: asString(pinixJson?.name) ?? asString(packageJson?.name),
|
|
175
|
+
version: asString(pinixJson?.version) ?? asString(packageJson?.version),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function asString(value: unknown): string | undefined {
|
|
180
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
150
183
|
export function createIPCManifest(clip: Clip): IPCManifest {
|
|
151
|
-
const
|
|
184
|
+
const pkgInfo = resolvePackageInfo();
|
|
152
185
|
|
|
153
186
|
return {
|
|
154
|
-
name: clip.name,
|
|
155
187
|
domain: clip.domain,
|
|
156
188
|
commands: Array.from(clip.getCommands().keys()),
|
|
157
|
-
dependencies:
|
|
158
|
-
|
|
159
|
-
: [],
|
|
189
|
+
dependencies: clip.dependencies,
|
|
190
|
+
...pkgInfo,
|
|
160
191
|
};
|
|
161
192
|
}
|
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
|
|