@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinixai/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.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/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
- type IPCRequest = {
5
- id?: unknown;
6
- command?: unknown;
7
- input?: unknown;
8
- };
4
+ // === IPC Protocol Types ===
9
5
 
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
- };
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
- function writeResponse(response: IPCResponse): void {
28
- process.stdout.write(`${JSON.stringify(response)}\n`);
8
+ interface BaseMessage {
9
+ type: MessageType;
10
+ id?: string;
29
11
  }
30
12
 
31
- function isObject(value: unknown): value is Record<string, unknown> {
32
- return value !== null && typeof value === "object";
13
+ interface RegisterMessage extends BaseMessage {
14
+ type: "register";
15
+ manifest: { name: string; domain: string; commands: string[]; dependencies: string[] };
33
16
  }
34
17
 
35
- function createErrorResponse(id: unknown, message: string, code: string): IPCResponse {
36
- return {
37
- id,
38
- error: {
39
- message,
40
- code,
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
- async function handleRequestLine(clip: Clip, line: string): Promise<void> {
46
- let request: IPCRequest;
26
+ interface ResultMessage extends BaseMessage {
27
+ type: "result";
28
+ id: string;
29
+ output: unknown;
30
+ }
47
31
 
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
- }
32
+ interface ErrorMessage extends BaseMessage {
33
+ type: "error";
34
+ id: string;
35
+ error: string;
36
+ }
54
37
 
55
- const { id = null, command, input } = request;
38
+ // === State ===
56
39
 
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
- }
40
+ const pendingInvokes = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
41
+ let idCounter = 0;
61
42
 
62
- const commandHandler = clip.getCommands().get(command);
43
+ function nextId(): string {
44
+ return `c${++idCounter}`;
45
+ }
63
46
 
64
- if (!commandHandler) {
65
- writeResponse(createErrorResponse(id, `Unknown command: ${command}`, "COMMAND_NOT_FOUND"));
66
- return;
67
- }
47
+ function send(msg: Record<string, unknown>): void {
48
+ process.stdout.write(JSON.stringify(msg) + "\n");
49
+ }
68
50
 
69
- if (!isObject(input)) {
70
- writeResponse(createErrorResponse(id, "Request input must be an object", "INVALID_INPUT"));
71
- return;
72
- }
51
+ // === Public API ===
73
52
 
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
- }
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
- 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(" ");
61
+ // === IPC Server ===
103
62
 
104
- process.stderr.write(`${serialized}\n`);
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
- export async function serveIPC(clip: Clip): Promise<void> {
109
- redirectConsoleLogToStderr();
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
- const reader = Bun.stdin.stream().getReader();
112
- const decoder = new TextDecoder();
113
- let buffer = "";
87
+ if (!msg.type) {
88
+ process.stderr.write(`[ipc] message missing type: ${line}\n`);
89
+ continue;
90
+ }
114
91
 
115
- try {
116
- while (true) {
117
- const { done, value } = await reader.read();
92
+ switch (msg.type) {
93
+ case "registered":
94
+ // Registration confirmed
95
+ break;
118
96
 
119
- if (done) {
97
+ case "invoke": {
98
+ const inv = msg as InvokeMessage;
99
+ handleInvoke(inv, commands);
120
100
  break;
121
101
  }
122
102
 
123
- buffer += decoder.decode(value, { stream: true });
124
-
125
- while (true) {
126
- const newlineIndex = buffer.indexOf("\n");
127
-
128
- if (newlineIndex === -1) {
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
- try {
140
- await handleRequestLine(clip, line);
141
- } catch (error) {
142
- writeResponse(createErrorResponse(null, toErrorMessage(error), "INTERNAL_ERROR"));
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
- buffer += decoder.decode();
148
- const tail = buffer.trim();
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
- if (tail.length > 0) {
151
- try {
152
- await handleRequestLine(clip, tail);
153
- } catch (error) {
154
- writeResponse(createErrorResponse(null, toErrorMessage(error), "INTERNAL_ERROR"));
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
+ }