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