@pinet/pinet-core 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Will Porcellini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @pinet/pinet-core
2
+
3
+ Runtime-core helpers for Pinet that are independent of any Slack adapter.
4
+
5
+ Current seam:
6
+
7
+ - Pinet output option normalization (`cli` default, explicit `json`/`full` opt-ins)
8
+ - durable Pinet read result text/detail formatting
9
+ - scheduled wake-up time parsing and thread ID helpers
10
+
11
+ `@pinet/slack-bridge` still composes the extension and preserves compatibility wrappers, but these helpers now live behind package exports so future extraction can move one boundary at a time.
12
+
13
+ Design proposal: `plans/slack-split-proposal.md`
14
+
15
+ ## Publishing
16
+
17
+ This package is included in the full npm publish set tracked in
18
+ [`../plans/npm-publish.md`](../plans/npm-publish.md). Use the GitHub Actions
19
+ workflow's default dry-run/readiness path for validation; do not publish, tag, or
20
+ bump versions without explicit maintainer release approval.
@@ -0,0 +1,3 @@
1
+ export * from "./output-options.js";
2
+ export * from "./pinet-read-formatting.js";
3
+ export * from "./scheduled-wakeups.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./output-options.js";
2
+ export * from "./pinet-read-formatting.js";
3
+ export * from "./scheduled-wakeups.js";
@@ -0,0 +1,6 @@
1
+ export type PinetOutputFormat = "cli" | "json";
2
+ export interface PinetOutputOptions {
3
+ format: PinetOutputFormat;
4
+ full: boolean;
5
+ }
6
+ export declare function normalizePinetOutputOptions(args: Record<string, unknown>): PinetOutputOptions;
@@ -0,0 +1,12 @@
1
+ export function normalizePinetOutputOptions(args) {
2
+ const rawFormat = args.format ?? args.f ?? args["-f"];
3
+ const format = rawFormat == null ? "cli" : String(rawFormat).trim().toLowerCase();
4
+ if (format !== "cli" && format !== "json") {
5
+ throw new Error('format must be "cli" or "json".');
6
+ }
7
+ const rawFull = args.full ?? args["--full"];
8
+ if (rawFull != null && typeof rawFull !== "boolean") {
9
+ throw new Error("full must be a boolean when provided.");
10
+ }
11
+ return { format, full: rawFull === true };
12
+ }
@@ -0,0 +1,49 @@
1
+ import { type PinetMailClass } from "@pinet/broker-core/mail-classification";
2
+ export interface PinetInboxItem {
3
+ inboxId: number;
4
+ delivered: boolean;
5
+ readAt: string | null;
6
+ message: {
7
+ id: number;
8
+ threadId: string;
9
+ source: string;
10
+ direction: string;
11
+ sender: string;
12
+ body: string;
13
+ metadata: Record<string, unknown> | null;
14
+ createdAt: string;
15
+ };
16
+ }
17
+ export interface PinetReadMessage {
18
+ inboxId: number;
19
+ delivered: boolean;
20
+ readAt: string | null;
21
+ message: PinetInboxItem["message"];
22
+ }
23
+ export interface PinetUnreadThreadSummary {
24
+ threadId: string;
25
+ source: string;
26
+ channel: string;
27
+ unreadCount: number;
28
+ latestMessageId: number;
29
+ latestAt: string;
30
+ highestMailClass: PinetMailClass;
31
+ mailClassCounts: Record<PinetMailClass, number>;
32
+ }
33
+ export interface PinetReadResult {
34
+ messages: PinetReadMessage[];
35
+ unreadCountBefore: number;
36
+ unreadCountAfter: number;
37
+ unreadThreads: PinetUnreadThreadSummary[];
38
+ markedReadIds: number[];
39
+ }
40
+ export interface PinetReadOptions {
41
+ threadId?: string;
42
+ limit?: number;
43
+ unreadOnly?: boolean;
44
+ markRead?: boolean;
45
+ }
46
+ export declare function formatPinetReadResultFull(result: PinetReadResult, options: PinetReadOptions): string;
47
+ export declare function summarizeUnreadThreadCounts(thread: PinetUnreadThreadSummary): string;
48
+ export declare function buildCompactPinetReadDetails(result: PinetReadResult): Record<string, unknown>;
49
+ export declare function formatPinetReadResultCompact(result: PinetReadResult, options: PinetReadOptions): string;
@@ -0,0 +1,94 @@
1
+ import { classifyPinetMail, formatPinetMailClassLabel, } from "@pinet/broker-core/mail-classification";
2
+ function truncateText(value, maxLength = 180) {
3
+ const collapsed = value.replace(/\s+/g, " ").trim();
4
+ if (collapsed.length <= maxLength)
5
+ return collapsed;
6
+ return `${collapsed.slice(0, Math.max(0, maxLength - 1))}…`;
7
+ }
8
+ export function formatPinetReadResultFull(result, options) {
9
+ const scope = options.threadId ? `thread ${options.threadId}` : "your Pinet inbox";
10
+ const mode = options.unreadOnly === false ? "latest" : "unread";
11
+ const lines = [
12
+ `Pinet read (${mode}) from ${scope}: ${result.messages.length} message${result.messages.length === 1 ? "" : "s"}.`,
13
+ `Unread before: ${result.unreadCountBefore}; unread after: ${result.unreadCountAfter}.`,
14
+ ];
15
+ if (result.messages.length > 0) {
16
+ lines.push("");
17
+ for (const item of result.messages) {
18
+ const classification = classifyPinetMail({
19
+ source: item.message.source,
20
+ threadId: item.message.threadId,
21
+ sender: item.message.sender,
22
+ body: item.message.body,
23
+ metadata: item.message.metadata,
24
+ });
25
+ const label = formatPinetMailClassLabel(classification.class);
26
+ lines.push(`- [${label}] [${item.message.source}/${item.message.threadId} #${item.message.id}] ${item.message.sender}: ${item.message.body}`);
27
+ }
28
+ }
29
+ if (result.unreadThreads.length > 0) {
30
+ lines.push("", "Unread thread pointers:");
31
+ for (const thread of result.unreadThreads.slice(0, 10)) {
32
+ const label = formatPinetMailClassLabel(thread.highestMailClass);
33
+ const counts = summarizeUnreadThreadCounts(thread);
34
+ lines.push(`- [${label}] ${thread.threadId} (${thread.source}${thread.channel ? `/${thread.channel}` : ""}): ${thread.unreadCount} unread${counts ? ` (${counts})` : ""}; latest #${thread.latestMessageId}; pointer=pinet action=read args.thread_id=${thread.threadId} args.unread_only=true`);
35
+ }
36
+ }
37
+ if (result.markedReadIds.length > 0) {
38
+ lines.push("", `Marked read: ${result.markedReadIds.join(", ")}.`);
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+ export function summarizeUnreadThreadCounts(thread) {
43
+ return [
44
+ thread.mailClassCounts.steering > 0 ? `${thread.mailClassCounts.steering} steering` : null,
45
+ thread.mailClassCounts.fwup > 0 ? `${thread.mailClassCounts.fwup} fwup` : null,
46
+ thread.mailClassCounts.maintenance_context > 0
47
+ ? `${thread.mailClassCounts.maintenance_context} maintenance/context`
48
+ : null,
49
+ ]
50
+ .filter((item) => Boolean(item))
51
+ .join(", ");
52
+ }
53
+ export function buildCompactPinetReadDetails(result) {
54
+ return {
55
+ messageCount: result.messages.length,
56
+ unreadCountBefore: result.unreadCountBefore,
57
+ unreadCountAfter: result.unreadCountAfter,
58
+ markedReadIds: result.markedReadIds,
59
+ messages: result.messages.map((item) => {
60
+ const classification = classifyPinetMail({
61
+ source: item.message.source,
62
+ threadId: item.message.threadId,
63
+ sender: item.message.sender,
64
+ body: item.message.body,
65
+ metadata: item.message.metadata,
66
+ });
67
+ return {
68
+ inboxId: item.inboxId,
69
+ messageId: item.message.id,
70
+ threadId: item.message.threadId,
71
+ source: item.message.source,
72
+ sender: item.message.sender,
73
+ class: classification.class,
74
+ preview: truncateText(item.message.body),
75
+ };
76
+ }),
77
+ unreadThreads: result.unreadThreads.slice(0, 10).map((thread) => ({
78
+ threadId: thread.threadId,
79
+ source: thread.source,
80
+ unreadCount: thread.unreadCount,
81
+ latestMessageId: thread.latestMessageId,
82
+ highestMailClass: thread.highestMailClass,
83
+ mailClassSummary: summarizeUnreadThreadCounts(thread),
84
+ })),
85
+ };
86
+ }
87
+ export function formatPinetReadResultCompact(result, options) {
88
+ const mode = options.unreadOnly === false ? "latest" : "unread";
89
+ const markedSuffix = result.markedReadIds.length > 0 ? `; marked ${result.markedReadIds.length}` : "";
90
+ const unreadThreadSuffix = result.unreadThreads.length > 0
91
+ ? `; ${result.unreadThreads.length} unread thread${result.unreadThreads.length === 1 ? "" : "s"}`
92
+ : "";
93
+ return `Pinet read: ${result.messages.length} ${mode} message${result.messages.length === 1 ? "" : "s"}; unread ${result.unreadCountBefore}→${result.unreadCountAfter}${markedSuffix}${unreadThreadSuffix}.`;
94
+ }
@@ -0,0 +1,7 @@
1
+ export interface ScheduledWakeupInput {
2
+ delay?: string;
3
+ at?: string;
4
+ }
5
+ export declare function parseScheduledWakeupDelay(delay: string): number | null;
6
+ export declare function resolveScheduledWakeupFireAt(input: ScheduledWakeupInput, now?: number): string;
7
+ export declare function buildScheduledWakeupThreadId(agentId: string): string;
@@ -0,0 +1,57 @@
1
+ const DELAY_UNITS_MS = {
2
+ ms: 1,
3
+ s: 1_000,
4
+ m: 60_000,
5
+ h: 60 * 60_000,
6
+ d: 24 * 60 * 60_000,
7
+ };
8
+ export function parseScheduledWakeupDelay(delay) {
9
+ const normalized = delay.trim().toLowerCase().replace(/\s+/g, "");
10
+ if (!normalized) {
11
+ return null;
12
+ }
13
+ const tokenRegex = /(\d+)(ms|s|m|h|d)/g;
14
+ let totalMs = 0;
15
+ let matchedLength = 0;
16
+ for (const match of normalized.matchAll(tokenRegex)) {
17
+ const [token, amountText, unit] = match;
18
+ if (!token || !amountText || !unit || match.index !== matchedLength) {
19
+ return null;
20
+ }
21
+ const amount = Number.parseInt(amountText, 10);
22
+ if (!Number.isFinite(amount) || amount < 0) {
23
+ return null;
24
+ }
25
+ totalMs += amount * DELAY_UNITS_MS[unit];
26
+ matchedLength += token.length;
27
+ }
28
+ if (matchedLength !== normalized.length || totalMs <= 0) {
29
+ return null;
30
+ }
31
+ return totalMs;
32
+ }
33
+ export function resolveScheduledWakeupFireAt(input, now = Date.now()) {
34
+ const hasDelay = typeof input.delay === "string" && input.delay.trim().length > 0;
35
+ const hasAt = typeof input.at === "string" && input.at.trim().length > 0;
36
+ if (hasDelay === hasAt) {
37
+ throw new Error("Provide exactly one of delay or at.");
38
+ }
39
+ if (hasDelay) {
40
+ const delayMs = parseScheduledWakeupDelay(input.delay);
41
+ if (delayMs == null) {
42
+ throw new Error("Invalid delay. Use values like 5m, 30s, 1h30m, or 1d.");
43
+ }
44
+ return new Date(now + delayMs).toISOString();
45
+ }
46
+ const fireAtMs = Date.parse(input.at);
47
+ if (Number.isNaN(fireAtMs)) {
48
+ throw new Error("Invalid timestamp. Use an ISO-8601 time like 2026-04-02T14:30:00Z.");
49
+ }
50
+ if (fireAtMs <= now) {
51
+ throw new Error("Scheduled wake-up time must be in the future.");
52
+ }
53
+ return new Date(fireAtMs).toISOString();
54
+ }
55
+ export function buildScheduledWakeupThreadId(agentId) {
56
+ return `wakeup:${agentId}`;
57
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pinet/pinet-core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Pinet runtime core for pi — broker/follower orchestration and mesh tooling",
6
+ "author": "Will Porcellini <5994936+gugu91@users.noreply.github.com>",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/gugu91/extensions.git",
11
+ "directory": "pinet-core"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "exports": {
18
+ ".": "./dist/index.js",
19
+ "./output-options": "./dist/output-options.js",
20
+ "./pinet-read-formatting": "./dist/pinet-read-formatting.js",
21
+ "./scheduled-wakeups": "./dist/scheduled-wakeups.js",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "README.md",
26
+ "LICENSE",
27
+ "dist/"
28
+ ],
29
+ "pi": {},
30
+ "scripts": {
31
+ "build": "node ../scripts/build-package.mjs",
32
+ "prepack": "pnpm run build",
33
+ "lint": "eslint . --ext .ts",
34
+ "typecheck": "tsc --noEmit",
35
+ "test": "vitest run *.test.ts"
36
+ },
37
+ "dependencies": {
38
+ "@pinet/broker-core": "0.1.0",
39
+ "@pinet/transport-core": "0.1.0"
40
+ },
41
+ "types": "./dist/index.d.ts"
42
+ }