@pinixai/core 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pinixai/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -0,0 +1,8 @@
1
+ export interface Binding {
2
+ alias: string;
3
+ hub?: string;
4
+ hub_token?: string;
5
+ clip_token?: string;
6
+ }
7
+
8
+ export type Bindings = Record<string, Binding>;
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.parseInt(value, 10);
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.parseInt(value, 10);
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.parseInt(portArg, 10);
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
- `${this.name} (${this.domain})`,
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 { Clip } from "./clip";
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 { 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: { name: string; domain: string; commands: string[]; dependencies: string[] };
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(clip: string, command: string, input: unknown): Promise<unknown> {
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({ id, type: "invoke", clip, command, input });
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
- const origLog = console.log;
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
- // Registration confirmed
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.parse(msg.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
- send({ id: msg.id, type: "result", output });
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 += chunk.toString();
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,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 { Clip } from "./clip";
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 lines = [
98
- `Clip: ${clip.name}`,
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 dependencies = (clip as Clip & { dependencies?: unknown }).dependencies;
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: Array.isArray(dependencies)
158
- ? dependencies.filter((dependency): dependency is string => typeof dependency === "string")
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 { Clip } from "./clip";
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