@pi-unipi/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/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @unipi/core
2
+
3
+ Shared utilities, event types, and constants for the [Unipi](https://github.com/Neuron-Mr-White/unipi) extension suite.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:@unipi/core
9
+ ```
10
+
11
+ Or as part of the full suite:
12
+ ```bash
13
+ pi install npm:unipi
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```typescript
19
+ import { UNIPI_EVENTS, MODULES, sanitize, emitEvent } from "@unipi/core";
20
+
21
+ // Emit module ready event
22
+ emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
23
+ name: MODULES.WORKFLOW,
24
+ version: "1.0.0",
25
+ commands: ["brainstorm", "plan"],
26
+ tools: [],
27
+ });
28
+
29
+ // Use shared utilities
30
+ const safeName = sanitize("my/feature: branch");
31
+ ```
32
+
33
+ ## Exports
34
+
35
+ ### Constants
36
+ - `UNIPI_PREFIX` — Command prefix (`unipi:`)
37
+ - `MODULES` — All module names
38
+ - `WORKFLOW_COMMANDS` — Workflow command names
39
+ - `RALPH_COMMANDS` — Ralph command names
40
+ - `RALPH_TOOLS` — Ralph tool names
41
+ - `RALPH_DEFAULTS` — Default ralph settings
42
+ - `RALPH_DIR` — Ralph state directory
43
+ - `RALPH_COMPLETE_MARKER` — Loop completion marker
44
+
45
+ ### Events
46
+ - `UNIPI_EVENTS` — Event names
47
+ - `UnipiModuleEvent` — Module ready/gone payload
48
+ - `UnipiWorkflowEvent` — Workflow start/end payload
49
+ - `UnipiRalphLoopEvent` — Ralph loop start/end payload
50
+ - `UnipiRalphIterationEvent` — Ralph iteration payload
51
+ - `UnipiStatusRequestEvent` / `UnipiStatusResponseEvent` — Status payloads
52
+
53
+ ### Utilities
54
+ - `sanitize(name)` — Sanitize string for filenames
55
+ - `ensureDir(path)` — Create parent directories
56
+ - `tryDelete(path)` — Safe file deletion
57
+ - `tryRead(path)` — Safe file read
58
+ - `safeMtimeMs(path)` — File modification time
59
+ - `tryRemoveDir(path)` — Safe directory removal
60
+ - `resolvePath(cwd, path)` — Resolve relative/absolute paths
61
+ - `fileExists(path)` — Check file existence
62
+ - `writeFile(path, content)` — Write file with dir creation
63
+ - `readJson<T>(path)` — Read JSON file
64
+ - `writeJson(path, data)` — Write JSON file
65
+ - `randomId(length)` — Generate random ID
66
+ - `now()` — ISO timestamp
67
+ - `parseArgs(str)` — Parse quoted arguments
68
+ - `getPackageVersion(dir)` — Read package version
69
+ - `isModuleAvailable(cwd, name)` — Check if npm module exists
70
+ - `emitEvent(pi, name, payload)` — Safe event emission
71
+
72
+ ## License
73
+
74
+ MIT
package/constants.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @unipi/core — Shared constants
3
+ */
4
+
5
+ /** Prefix for all unipi commands */
6
+ export const UNIPI_PREFIX = "unipi:" as const;
7
+
8
+ /** Ralph loop state directory */
9
+ export const RALPH_DIR = ".unipi/ralph" as const;
10
+
11
+ /** Ralph completion marker */
12
+ export const RALPH_COMPLETE_MARKER = "<promise>COMPLETE</promise>" as const;
13
+
14
+ /** Unipi settings key in pi settings.json */
15
+ export const UNIPI_SETTINGS_KEY = "unipi" as const;
16
+
17
+ /** Module names */
18
+ export const MODULES = {
19
+ CORE: "@unipi/core",
20
+ WORKFLOW: "@unipi/workflow",
21
+ RALPH: "@unipi/ralph",
22
+ SUBAGENTS: "@unipi/subagents",
23
+ MEMORY: "@unipi/memory",
24
+ REGISTRY: "@unipi/registry",
25
+ MCP: "@unipi/mcp",
26
+ TASK: "@unipi/task",
27
+ WEBTOOLS: "@unipi/webtools",
28
+ INFO_SCREEN: "@unipi/info-screen",
29
+ IMPECCABLE: "@unipi/impeccable",
30
+ SETTINGS: "@unipi/settings",
31
+ } as const;
32
+
33
+ /** Workflow command names */
34
+ export const WORKFLOW_COMMANDS = {
35
+ BRAINSTORM: "brainstorm",
36
+ PLAN: "plan",
37
+ WORK: "work",
38
+ REVIEW_WORK: "review-work",
39
+ CONSOLIDATE: "consolidate",
40
+ WORKTREE_CREATE: "worktree-create",
41
+ WORKTREE_LIST: "worktree-list",
42
+ WORKTREE_MERGE: "worktree-merge",
43
+ CONSULTANT: "consultant",
44
+ QUICK_WORK: "quick-work",
45
+ GATHER_CONTEXT: "gather-context",
46
+ DOCUMENT: "document",
47
+ SCAN_ISSUES: "scan-issues",
48
+ } as const;
49
+
50
+ /** Ralph command names */
51
+ export const RALPH_COMMANDS = {
52
+ START: "ralph-start",
53
+ STOP: "ralph-stop",
54
+ RESUME: "ralph-resume",
55
+ STATUS: "ralph-status",
56
+ CANCEL: "ralph-cancel",
57
+ ARCHIVE: "ralph-archive",
58
+ CLEAN: "ralph-clean",
59
+ LIST: "ralph-list",
60
+ NUKE: "ralph-nuke",
61
+ } as const;
62
+
63
+ /** Ralph tool names */
64
+ export const RALPH_TOOLS = {
65
+ START: "ralph_start",
66
+ DONE: "ralph_done",
67
+ } as const;
68
+
69
+ /** Unipi directory paths */
70
+ export const UNIPI_DIRS = {
71
+ ROOT: ".unipi",
72
+ DOCS: ".unipi/docs",
73
+ SPECS: ".unipi/docs/specs",
74
+ PLANS: ".unipi/docs/plans",
75
+ GENERATED: ".unipi/docs/generated",
76
+ REVIEWS: ".unipi/docs/reviews",
77
+ MEMORY: ".unipi/memory",
78
+ QUICK_WORK: ".unipi/quick-work",
79
+ } as const;
80
+
81
+ /** Default ralph loop settings */
82
+ export const RALPH_DEFAULTS = {
83
+ MAX_ITERATIONS: 50,
84
+ ITEMS_PER_ITERATION: 0,
85
+ REFLECT_EVERY: 0,
86
+ } as const;
87
+
88
+ /** Status icons for ralph loops */
89
+ export const RALPH_STATUS_ICONS = {
90
+ active: "▶",
91
+ paused: "⏸",
92
+ completed: "✓",
93
+ } as const;
package/events.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @unipi/core — Event type definitions for inter-module communication
3
+ *
4
+ * Modules announce presence via pi.events. Other modules listen and
5
+ * enable integration features when peers are detected.
6
+ */
7
+
8
+ /** Event names emitted by unipi modules */
9
+ export const UNIPI_EVENTS = {
10
+ /** Module loaded and ready */
11
+ MODULE_READY: "unipi:module:ready",
12
+ /** Module unloading */
13
+ MODULE_GONE: "unipi:module:gone",
14
+
15
+ /** Workflow command started */
16
+ WORKFLOW_START: "unipi:workflow:start",
17
+ /** Workflow command ended */
18
+ WORKFLOW_END: "unipi:workflow:end",
19
+
20
+ /** Ralph loop started */
21
+ RALPH_LOOP_START: "unipi:ralph:loop:start",
22
+ /** Ralph loop ended */
23
+ RALPH_LOOP_END: "unipi:ralph:loop:end",
24
+ /** Ralph loop iteration completed */
25
+ RALPH_ITERATION_DONE: "unipi:ralph:iteration:done",
26
+
27
+ /** Request module status (for info-screen) */
28
+ MODULE_STATUS_REQUEST: "unipi:module:status:request",
29
+ /** Module status response */
30
+ MODULE_STATUS_RESPONSE: "unipi:module:status:response",
31
+ } as const;
32
+
33
+ /** Payload for MODULE_READY / MODULE_GONE */
34
+ export interface UnipiModuleEvent {
35
+ /** Module name, e.g. "@unipi/workflow" */
36
+ name: string;
37
+ /** Module version */
38
+ version: string;
39
+ /** Commands registered by this module */
40
+ commands: string[];
41
+ /** Tools registered by this module */
42
+ tools: string[];
43
+ }
44
+
45
+ /** Payload for WORKFLOW_START / WORKFLOW_END */
46
+ export interface UnipiWorkflowEvent {
47
+ /** Command name, e.g. "brainstorm" */
48
+ command: string;
49
+ /** Full command with prefix, e.g. "/unipi:brainstorm" */
50
+ fullCommand: string;
51
+ /** Arguments passed to command */
52
+ args: string;
53
+ /** For WORKFLOW_END: whether it succeeded */
54
+ success?: boolean;
55
+ /** For WORKFLOW_END: duration in ms */
56
+ durationMs?: number;
57
+ }
58
+
59
+ /** Payload for RALPH_LOOP_START / RALPH_LOOP_END */
60
+ export interface UnipiRalphLoopEvent {
61
+ /** Loop name */
62
+ name: string;
63
+ /** Current iteration */
64
+ iteration: number;
65
+ /** Max iterations (0 = unlimited) */
66
+ maxIterations: number;
67
+ /** Loop status */
68
+ status: "active" | "paused" | "completed";
69
+ /** For RALPH_LOOP_END: reason */
70
+ reason?: "completed" | "max_reached" | "cancelled" | "error";
71
+ }
72
+
73
+ /** Payload for RALPH_ITERATION_DONE */
74
+ export interface UnipiRalphIterationEvent {
75
+ /** Loop name */
76
+ name: string;
77
+ /** Iteration that just completed */
78
+ iteration: number;
79
+ /** Next iteration number */
80
+ nextIteration: number;
81
+ }
82
+
83
+ /** Payload for MODULE_STATUS_REQUEST */
84
+ export interface UnipiStatusRequestEvent {
85
+ /** Request ID for correlation */
86
+ requestId: string;
87
+ }
88
+
89
+ /** Payload for MODULE_STATUS_RESPONSE */
90
+ export interface UnipiStatusResponseEvent {
91
+ /** Request ID this responds to */
92
+ requestId: string;
93
+ /** Module name */
94
+ name: string;
95
+ /** Module status data */
96
+ status: Record<string, unknown>;
97
+ }
98
+
99
+ /** Union of all unipi event payloads */
100
+ export type UnipiEventPayload =
101
+ | UnipiModuleEvent
102
+ | UnipiWorkflowEvent
103
+ | UnipiRalphLoopEvent
104
+ | UnipiRalphIterationEvent
105
+ | UnipiStatusRequestEvent
106
+ | UnipiStatusResponseEvent;
package/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @unipi/core — Shared utilities for Unipi extension suite
3
+ *
4
+ * Re-exports all constants, events, and utilities.
5
+ */
6
+
7
+ export * from "./constants.js";
8
+ export * from "./events.js";
9
+ export * from "./sandbox.js";
10
+ export * from "./utils.js";
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@pi-unipi/core",
3
+ "version": "0.1.0",
4
+ "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Neuron Mr White",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Neuron-Mr-White/unipi.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi-extension",
16
+ "unipi"
17
+ ],
18
+ "files": [
19
+ "*.ts",
20
+ "README.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "peerDependencies": {
26
+ "@mariozechner/pi-coding-agent": "*",
27
+ "@sinclair/typebox": "*"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0"
31
+ }
32
+ }
package/sandbox.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @unipi/core — Sandbox module
3
+ *
4
+ * Defines tool access levels for workflow commands.
5
+ * Used with pi.setActiveTools() to enforce restrictions.
6
+ */
7
+
8
+ import { WORKFLOW_COMMANDS } from "./constants.js";
9
+
10
+ /** Sandbox levels */
11
+ export type SandboxLevel = "read_only" | "brainstorm" | "write_unipi" | "full";
12
+
13
+ /** Tool sets per sandbox level */
14
+ const SANDBOX_TOOLS: Record<SandboxLevel, readonly string[]> = {
15
+ /** Only read-only tools — no bash, no write, no edit */
16
+ read_only: ["read", "grep", "find", "ls"],
17
+ /** Read + constrained write — only to .unipi/docs/specs/ */
18
+ brainstorm: ["read", "grep", "find", "ls", "write"],
19
+ /** Read + write/edit — bash blocked, writes go through write tool */
20
+ write_unipi: ["read", "write", "edit"],
21
+ /** All tools */
22
+ full: ["read", "write", "edit", "bash"],
23
+ };
24
+
25
+ /** Command to sandbox level mapping */
26
+ const COMMAND_SANDBOX: Record<string, SandboxLevel> = {
27
+ [WORKFLOW_COMMANDS.BRAINSTORM]: "brainstorm",
28
+ [WORKFLOW_COMMANDS.PLAN]: "write_unipi",
29
+ [WORKFLOW_COMMANDS.WORK]: "full",
30
+ [WORKFLOW_COMMANDS.REVIEW_WORK]: "read_only",
31
+ [WORKFLOW_COMMANDS.CONSOLIDATE]: "write_unipi",
32
+ [WORKFLOW_COMMANDS.WORKTREE_CREATE]: "full",
33
+ [WORKFLOW_COMMANDS.WORKTREE_LIST]: "read_only",
34
+ [WORKFLOW_COMMANDS.WORKTREE_MERGE]: "full",
35
+ [WORKFLOW_COMMANDS.CONSULTANT]: "read_only",
36
+ [WORKFLOW_COMMANDS.QUICK_WORK]: "full",
37
+ [WORKFLOW_COMMANDS.GATHER_CONTEXT]: "read_only",
38
+ [WORKFLOW_COMMANDS.DOCUMENT]: "write_unipi",
39
+ [WORKFLOW_COMMANDS.SCAN_ISSUES]: "read_only",
40
+ };
41
+
42
+ /**
43
+ * Get sandbox level for a command.
44
+ */
45
+ export function getSandboxLevel(commandName: string): SandboxLevel {
46
+ return COMMAND_SANDBOX[commandName] ?? "full";
47
+ }
48
+
49
+ /**
50
+ * Get allowed tools for a sandbox level.
51
+ */
52
+ export function getToolsForLevel(level: SandboxLevel): readonly string[] {
53
+ return SANDBOX_TOOLS[level];
54
+ }
55
+
56
+ /**
57
+ * Get allowed tools for a command.
58
+ */
59
+ export function getToolsForCommand(commandName: string): readonly string[] {
60
+ const level = getSandboxLevel(commandName);
61
+ return getToolsForLevel(level);
62
+ }
63
+
64
+ /**
65
+ * Check if a tool is allowed at a sandbox level.
66
+ */
67
+ export function isToolAllowed(level: SandboxLevel, toolName: string): boolean {
68
+ return SANDBOX_TOOLS[level].includes(toolName);
69
+ }
70
+
71
+ /**
72
+ * Check if a command has write access.
73
+ */
74
+ export function hasWriteAccess(commandName: string): boolean {
75
+ const level = getSandboxLevel(commandName);
76
+ return level === "write_unipi" || level === "full";
77
+ }
78
+
79
+ /**
80
+ * Check if a command has bash access.
81
+ */
82
+ export function hasBashAccess(commandName: string): boolean {
83
+ const level = getSandboxLevel(commandName);
84
+ return level === "full";
85
+ }
package/utils.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @unipi/core — Shared utility functions
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+
8
+ /**
9
+ * Sanitize a string for use as a filename.
10
+ * Replaces non-alphanumeric chars with underscores, collapses repeats.
11
+ */
12
+ export function sanitize(name: string): string {
13
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_");
14
+ }
15
+
16
+ /**
17
+ * Ensure parent directory exists for a file path.
18
+ */
19
+ export function ensureDir(filePath: string): void {
20
+ const dir = path.dirname(filePath);
21
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
22
+ }
23
+
24
+ /**
25
+ * Try to delete a file. Ignores errors.
26
+ */
27
+ export function tryDelete(filePath: string): void {
28
+ try {
29
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
30
+ } catch {
31
+ /* ignore */
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Try to read a file. Returns null on error.
37
+ */
38
+ export function tryRead(filePath: string): string | null {
39
+ try {
40
+ return fs.readFileSync(filePath, "utf-8");
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Get file mtime in ms. Returns 0 if file doesn't exist.
48
+ */
49
+ export function safeMtimeMs(filePath: string): number {
50
+ try {
51
+ return fs.statSync(filePath).mtimeMs;
52
+ } catch {
53
+ return 0;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Try to remove a directory recursively. Returns true on success.
59
+ */
60
+ export function tryRemoveDir(dirPath: string): boolean {
61
+ try {
62
+ if (fs.existsSync(dirPath)) {
63
+ fs.rmSync(dirPath, { recursive: true, force: true });
64
+ }
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Resolve a path relative to cwd, handling absolute paths.
73
+ */
74
+ export function resolvePath(cwd: string, filePath: string): string {
75
+ return path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
76
+ }
77
+
78
+ /**
79
+ * Check if a file exists.
80
+ */
81
+ export function fileExists(filePath: string): boolean {
82
+ return fs.existsSync(filePath);
83
+ }
84
+
85
+ /**
86
+ * Write a file, ensuring parent directory exists.
87
+ */
88
+ export function writeFile(filePath: string, content: string): void {
89
+ ensureDir(filePath);
90
+ fs.writeFileSync(filePath, content, "utf-8");
91
+ }
92
+
93
+ /**
94
+ * Read JSON file, return null on error.
95
+ */
96
+ export function readJson<T>(filePath: string): T | null {
97
+ const content = tryRead(filePath);
98
+ if (!content) return null;
99
+ try {
100
+ return JSON.parse(content) as T;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Write JSON file with pretty printing.
108
+ */
109
+ export function writeJson(filePath: string, data: unknown): void {
110
+ writeFile(filePath, JSON.stringify(data, null, 2));
111
+ }
112
+
113
+ /**
114
+ * Generate a short random ID.
115
+ */
116
+ export function randomId(length = 8): string {
117
+ return Math.random().toString(36).substring(2, 2 + length);
118
+ }
119
+
120
+ /**
121
+ * Format timestamp to ISO string.
122
+ */
123
+ export function now(): string {
124
+ return new Date().toISOString();
125
+ }
126
+
127
+ /**
128
+ * Parse command arguments string into tokens.
129
+ * Handles quoted strings.
130
+ */
131
+ export function parseArgs(argsStr: string): string[] {
132
+ return argsStr.match(/(?:[^\s"]+|"[^"]*")+/g)?.map((t) => t.replace(/^"|"$/g, "")) ?? [];
133
+ }
134
+
135
+ /**
136
+ * Get package version from package.json.
137
+ */
138
+ export function getPackageVersion(packageDir: string): string {
139
+ const pkgPath = path.join(packageDir, "package.json");
140
+ const pkg = readJson<{ version?: string }>(pkgPath);
141
+ return pkg?.version ?? "0.0.0";
142
+ }
143
+
144
+ /**
145
+ * Check if a module is available in node_modules.
146
+ */
147
+ export function isModuleAvailable(cwd: string, moduleName: string): boolean {
148
+ try {
149
+ const resolved = path.join(cwd, "node_modules", moduleName);
150
+ return fs.existsSync(resolved);
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Initialize .unipi directory structure.
158
+ * Creates all standard directories if they don't exist.
159
+ * Call on session_start in each extension.
160
+ */
161
+ export function initUnipiDirs(cwd: string = process.cwd()): void {
162
+ const dirs = [
163
+ ".unipi",
164
+ ".unipi/docs",
165
+ ".unipi/docs/specs",
166
+ ".unipi/docs/plans",
167
+ ".unipi/docs/generated",
168
+ ".unipi/docs/reviews",
169
+ ".unipi/memory",
170
+ ".unipi/quick-work",
171
+ ".unipi/worktrees",
172
+ ];
173
+ for (const dir of dirs) {
174
+ const full = path.join(cwd, dir);
175
+ if (!fs.existsSync(full)) {
176
+ fs.mkdirSync(full, { recursive: true });
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Emit a unipi event via pi.events (safe wrapper).
183
+ * Returns true if event was emitted.
184
+ */
185
+ export function emitEvent(
186
+ pi: { events: { emit: (name: string, payload: unknown) => void } },
187
+ eventName: string,
188
+ payload: unknown,
189
+ ): boolean {
190
+ try {
191
+ pi.events.emit(eventName, payload);
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }