@pimote/sdk 0.13.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 Antoine Lennartz
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,193 @@
1
+ # @pimote/sdk
2
+
3
+ Pimote's extensibility SDK — everything a [pi](https://github.com/mariozechner/pi-coding-agent) extension imports to talk to [pimote](https://github.com/alennartz/pimote). One subpath module per extension seam:
4
+
5
+ - **`@pimote/sdk/panels`** — push structured card data to the pimote web client
6
+ - **`@pimote/sdk/projects`** — contribute project discovery and creation to pimote's project layer
7
+
8
+ The root `@pimote/sdk` import re-exports both modules.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @pimote/sdk
14
+ ```
15
+
16
+ Requires `@earendil-works/pi-coding-agent` as an optional peer dependency (already present in any pi extension; only the panels seam imports from it).
17
+
18
+ ## Panels — `@pimote/sdk/panels`
19
+
20
+ Push structured card data from pi extensions to the pimote web client.
21
+
22
+ When your extension is running inside pimote, `detect()` returns a handle for sending cards to the panel UI. When running in a normal pi terminal session, it returns `null` — your extension keeps working either way.
23
+
24
+ ### Usage
25
+
26
+ ```ts
27
+ import { detect } from '@pimote/sdk/panels';
28
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
29
+
30
+ const extension: ExtensionFactory = (pi) => {
31
+ const panels = detect(pi, 'my-extension');
32
+
33
+ if (panels) {
34
+ // Running inside pimote — push cards to the web UI
35
+ panels.updateCards([
36
+ {
37
+ id: 'status',
38
+ color: 'success',
39
+ header: { title: 'Build', tag: 'passed' },
40
+ body: [{ content: 'All 42 tests passed', style: 'text' }],
41
+ footer: ['2.3s'],
42
+ },
43
+ ]);
44
+ }
45
+
46
+ // Rest of your extension — works in both pimote and regular pi
47
+ };
48
+
49
+ export default extension;
50
+ ```
51
+
52
+ ### API
53
+
54
+ #### `detect(pi, key)`
55
+
56
+ Detects whether the extension is running inside pimote.
57
+
58
+ - **`pi`** — The `ExtensionAPI` object passed to your extension factory.
59
+ - **`key`** — A unique namespace string for your extension's cards. Cards from different keys don't interfere with each other.
60
+
61
+ Returns a `PanelHandle` if running inside pimote, or `null` otherwise.
62
+
63
+ Calling `detect()` again with the same key deactivates the previous handle (its methods become no-ops) and returns a new one.
64
+
65
+ #### `PanelHandle`
66
+
67
+ ```ts
68
+ interface PanelHandle {
69
+ /** Replace all cards for this namespace. Previous cards are discarded. */
70
+ updateCards(cards: Card[]): void;
71
+ /** Remove all cards for this namespace. */
72
+ clear(): void;
73
+ }
74
+ ```
75
+
76
+ #### `Card`
77
+
78
+ ```ts
79
+ interface Card {
80
+ id: string;
81
+ color?: 'accent' | 'success' | 'warning' | 'error' | 'muted';
82
+ header: {
83
+ title: string;
84
+ tag?: string;
85
+ };
86
+ body?: BodySection[];
87
+ footer?: string[];
88
+ href?: string;
89
+ }
90
+
91
+ interface BodySection {
92
+ content: string;
93
+ style: 'text' | 'code' | 'secondary';
94
+ }
95
+ ```
96
+
97
+ - **`id`** — Unique identifier for the card within your namespace.
98
+ - **`color`** — Optional color theme for the card.
99
+ - **`header.title`** — Card title, always visible.
100
+ - **`header.tag`** — Optional short label displayed next to the title.
101
+ - **`body`** — Optional content sections, each with a style (`text`, `code`, or `secondary`).
102
+ - **`footer`** — Optional array of short strings displayed at the bottom.
103
+ - **`href`** — Optional same-origin URL. When set, the client renders the
104
+ entire card as a clickable link (same-tab navigation). Use for cards
105
+ that point at a hosted resource — e.g. a bundle served by pimote's
106
+ static-host tool.
107
+
108
+ ### How it works
109
+
110
+ Detection uses pi's EventBus for a synchronous in-process round-trip — pimote's server listens for `pimote:detect:request` and responds on `pimote:detect:response`. Card updates are emitted on the `pimote:panels` channel, which pimote's session manager picks up and pushes to the web client over WebSocket.
111
+
112
+ When pimote isn't present, the EventBus emit fires with no listener, `detect()` returns `null`, and there's zero overhead.
113
+
114
+ ## Projects — `@pimote/sdk/projects`
115
+
116
+ Type-only module for user-authored project-source modules: pimote scans a project-sources directory (default `~/.config/pimote/project-sources`, configurable via `projectSourcesDir`), dynamic-imports every JS/TS module it finds, and collects each module's exported `sources` and `creators` arrays. Sources contribute repos and multi-repo projects to pimote's project layer; creators make new project directories from user-supplied form params.
117
+
118
+ ### Writing a module
119
+
120
+ ```ts
121
+ // ~/.config/pimote/project-sources/my-source.ts
122
+ import type { ProjectSource, ProjectCreator } from '@pimote/sdk/projects';
123
+
124
+ export const sources: ProjectSource[] = [
125
+ {
126
+ id: 'my-source',
127
+ async list() {
128
+ return [
129
+ {
130
+ kind: 'repo',
131
+ path: '/home/me/work/api',
132
+ name: 'api',
133
+ branch: 'main',
134
+ dirty: false,
135
+ ahead: 0,
136
+ behind: 0,
137
+ },
138
+ ];
139
+ },
140
+ },
141
+ ];
142
+
143
+ export const creators: ProjectCreator[] = [
144
+ {
145
+ id: 'my-creator',
146
+ describe() {
147
+ return { label: 'New API project', paramSchema: { name: 'string', tags: 'string[]' } };
148
+ },
149
+ async create(params) {
150
+ const path = `/home/me/work/${String(params.name)}`;
151
+ // ... scaffold the project directory ...
152
+ return { path };
153
+ },
154
+ },
155
+ ];
156
+ ```
157
+
158
+ ### `ProjectSource`
159
+
160
+ The discovery seam. `list()` is called on cache miss and must return the current entries without mutating anything. The optional `onProjectOpen(projectPath)` hook is awaited before any open of a listed entry proceeds — whether the target exists on disk or not, and from every open path (row click, new session, manager tool). Sources self-filter by path: probe the disk and scaffold if the entry is theirs and missing; do other open-time work otherwise. A thrown error aborts the open and surfaces the message to the user.
161
+
162
+ ### `ProjectCreator`
163
+
164
+ The creation seam. `describe()` returns the human-readable form for the creation UI — a `label` plus a `paramSchema` mapping param names to `'string' | 'string[]'`. `create(params)` receives the user-supplied params, makes the project directory, and returns its `{ path }`.
165
+
166
+ ### Entry shapes
167
+
168
+ `list()` returns `SourceEntry[]` — a repo or a multi-repo project:
169
+
170
+ ```ts
171
+ /** Feeds a single-repo project. Extends `RepoInfo`. */
172
+ interface RepoSourceEntry extends RepoInfo {
173
+ kind: 'repo';
174
+ }
175
+
176
+ /** Feeds a multi-repo project over member paths. */
177
+ interface MultiRepoSourceEntry {
178
+ kind: 'project';
179
+ path: string;
180
+ name: string;
181
+ memberPaths: string[];
182
+ /** Source-contributed tags; shown to the user but not user-removable. */
183
+ tags?: string[];
184
+ }
185
+ ```
186
+
187
+ ### `RepoInfo`
188
+
189
+ Repo facts as contributed by sources: `path`, `name`, `branch` (`string | null`), `dirty`, `ahead`, `behind`, and optional `lastActivity` (epoch ms), `missing` (repo path no longer exists on disk), and `tags`. It mirrors pimote's wire `RepoInfo` type — same name, same fields — so extension authors can write `interface MyEntry extends RepoInfo` against the SDK alone.
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,2 @@
1
+ export * from './panels/index.js';
2
+ export * from './projects/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './panels/index.js';
2
+ export * from './projects/index.js';
@@ -0,0 +1,11 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import type { PanelHandle } from './types.js';
3
+ /**
4
+ * Detect whether running inside pimote. Returns a scoped handle for pushing
5
+ * card data, or null if not in pimote.
6
+ *
7
+ * Each key gets an independent namespace — cards from different keys don't
8
+ * interfere. Calling detect() again with the same key deactivates the
9
+ * previous handle (its methods become no-ops) and returns a new one.
10
+ */
11
+ export declare function detect(pi: ExtensionAPI, key: string): PanelHandle | null;
@@ -0,0 +1,48 @@
1
+ /** Tracks active state per key so re-detection deactivates the old handle.
2
+ * Scoped per ExtensionAPI instance (one per pi session) via a WeakMap, so two
3
+ * sessions in the same process that detect() the same key don't deactivate
4
+ * each other's handles. */
5
+ const handlesByApi = new WeakMap();
6
+ /**
7
+ * Detect whether running inside pimote. Returns a scoped handle for pushing
8
+ * card data, or null if not in pimote.
9
+ *
10
+ * Each key gets an independent namespace — cards from different keys don't
11
+ * interfere. Calling detect() again with the same key deactivates the
12
+ * previous handle (its methods become no-ops) and returns a new one.
13
+ */
14
+ export function detect(pi, key) {
15
+ // Synchronous detection round-trip
16
+ let detected = false;
17
+ const unsub = pi.events.on('pimote:detect:response', () => {
18
+ detected = true;
19
+ });
20
+ pi.events.emit('pimote:detect:request', {});
21
+ unsub();
22
+ if (!detected)
23
+ return null;
24
+ // Deactivate previous handle for this key (scoped to this pi instance)
25
+ let handles = handlesByApi.get(pi);
26
+ if (!handles) {
27
+ handles = new Map();
28
+ handlesByApi.set(pi, handles);
29
+ }
30
+ const prev = handles.get(key);
31
+ if (prev)
32
+ prev.active = false;
33
+ // Create new handle state
34
+ const state = { active: true };
35
+ handles.set(key, state);
36
+ return {
37
+ updateCards(cards) {
38
+ if (!state.active)
39
+ return;
40
+ pi.events.emit('pimote:panels', { type: 'cards', namespace: key, cards });
41
+ },
42
+ clear() {
43
+ if (!state.active)
44
+ return;
45
+ pi.events.emit('pimote:panels', { type: 'clear', namespace: key });
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,2 @@
1
+ export type { Card, CardColor, BodySection, BodySectionStyle, PanelHandle, PanelMessage } from './types.js';
2
+ export { detect } from './detect.js';
@@ -0,0 +1 @@
1
+ export { detect } from './detect.js';
@@ -0,0 +1,37 @@
1
+ export type CardColor = 'accent' | 'success' | 'warning' | 'error' | 'muted';
2
+ export type BodySectionStyle = 'text' | 'code' | 'secondary';
3
+ export interface BodySection {
4
+ content: string;
5
+ style: BodySectionStyle;
6
+ }
7
+ export interface Card {
8
+ id: string;
9
+ color?: CardColor;
10
+ header: {
11
+ title: string;
12
+ tag?: string;
13
+ };
14
+ body?: BodySection[];
15
+ footer?: string[];
16
+ /**
17
+ * Optional same-origin URL. When present, the client renders the entire
18
+ * card as a clickable link (same-tab navigation). Kept in lock-step with
19
+ * the protocol `Card` type in `shared/src/protocol.ts`.
20
+ */
21
+ href?: string;
22
+ }
23
+ export interface PanelHandle {
24
+ /** Replace this handle's cards. Full snapshot — previous cards for this namespace are discarded. */
25
+ updateCards(cards: Card[]): void;
26
+ /** Remove all cards for this namespace. */
27
+ clear(): void;
28
+ }
29
+ /** Message shapes emitted on the 'pimote:panels' EventBus channel. */
30
+ export type PanelMessage = {
31
+ type: 'cards';
32
+ namespace: string;
33
+ cards: Card[];
34
+ } | {
35
+ type: 'clear';
36
+ namespace: string;
37
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export type { ProjectSource, ProjectCreator, ProjectCreatorDescriptor, ProjectCreatorParamType, SourceEntry, RepoSourceEntry, MultiRepoSourceEntry, RepoInfo } from './types.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Repo facts as contributed by sources. Twin of the wire `RepoInfo` in
3
+ * shared/src/protocol.ts — drift-guarded by server/src/sdk-twins.ts.
4
+ */
5
+ export interface RepoInfo {
6
+ path: string;
7
+ name: string;
8
+ branch: string | null;
9
+ dirty: boolean;
10
+ /** Commits ahead of upstream; 0 when unknown. */
11
+ ahead: number;
12
+ behind: number;
13
+ /** Epoch ms of last session activity in this repo, when known. */
14
+ lastActivity?: number;
15
+ /** True when the repo path no longer exists on disk (deleted member, broken symlink). */
16
+ missing?: boolean;
17
+ /** Effective tags (user + source-contributed). Absent when untagged. */
18
+ tags?: string[];
19
+ }
20
+ /** Human-readable parameter type for a creator form field. */
21
+ export type ProjectCreatorParamType = 'string' | 'string[]';
22
+ /** A repo entry contributed to the discovery index (feeds single-repo projects). */
23
+ export interface RepoSourceEntry extends RepoInfo {
24
+ kind: 'repo';
25
+ }
26
+ /**
27
+ * A multi-repo project entry contributed to the project layer. `memberPaths`
28
+ * are concrete paths that may not exist on disk yet; the server derives
29
+ * existence state itself — sources never flag "virtual".
30
+ */
31
+ export interface MultiRepoSourceEntry {
32
+ kind: 'project';
33
+ path: string;
34
+ name: string;
35
+ memberPaths: string[];
36
+ /** Source-contributed tags; shown to the user but not user-removable. */
37
+ tags?: string[];
38
+ }
39
+ /** Anything a source lists: a repo, or a multi-repo project over member paths. */
40
+ export type SourceEntry = RepoSourceEntry | MultiRepoSourceEntry;
41
+ /**
42
+ * Extensible discovery seam: a source contributes repos and multi-repo
43
+ * projects to the project layer. Built in: the filesystem walker over
44
+ * configured roots. User-authored sources are dynamic-imported from the
45
+ * configured project-sources directory.
46
+ */
47
+ export interface ProjectSource {
48
+ readonly id: string;
49
+ /** Discover entries; called on cache miss. Must not mutate anything. */
50
+ list(): Promise<SourceEntry[]>;
51
+ /**
52
+ * Awaited before any open of a listed entry proceeds — whether the target
53
+ * exists on disk or not, and from every open path (row click, new session,
54
+ * manager tool). Sources self-filter by path: probe the disk and scaffold
55
+ * if the entry is theirs and missing; do other open-time work otherwise.
56
+ * A thrown error aborts the open and surfaces the message to the user.
57
+ */
58
+ onProjectOpen?(projectPath: string): Promise<void>;
59
+ }
60
+ /** Human-readable description of a creator's form, for the creation UI. */
61
+ export interface ProjectCreatorDescriptor {
62
+ label: string;
63
+ paramSchema: Record<string, ProjectCreatorParamType>;
64
+ }
65
+ /**
66
+ * Extensible creation seam: a creator makes a new project directory from
67
+ * user-supplied params. Built in: mkdir + git init. User-authored creators
68
+ * are dynamic-imported from the configured project-sources directory.
69
+ */
70
+ export interface ProjectCreator {
71
+ readonly id: string;
72
+ /** Human-readable description for the creation UI. */
73
+ describe(): ProjectCreatorDescriptor;
74
+ create(params: Record<string, unknown>): Promise<{
75
+ path: string;
76
+ }>;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@pimote/sdk",
3
+ "version": "0.13.0",
4
+ "description": "Pimote extensibility SDK: panel cards and project discovery/creation seams for pi extensions",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Antoine Lennartz <alennartz@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/alennartz/pimote.git",
11
+ "directory": "packages/sdk"
12
+ },
13
+ "homepage": "https://github.com/alennartz/pimote/tree/main/packages/sdk#readme",
14
+ "bugs": "https://github.com/alennartz/pimote/issues",
15
+ "keywords": [
16
+ "pi",
17
+ "pimote",
18
+ "extension",
19
+ "sdk",
20
+ "projects",
21
+ "panels",
22
+ "cards"
23
+ ],
24
+ "exports": {
25
+ ".": "./dist/index.js",
26
+ "./panels": "./dist/panels/index.js",
27
+ "./projects": "./dist/projects/index.js"
28
+ },
29
+ "types": "./dist/index.d.ts",
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "dev": "tsc --watch",
38
+ "test": "vitest",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "peerDependencies": {
42
+ "@earendil-works/pi-coding-agent": "^0.99.2"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "@earendil-works/pi-coding-agent": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-coding-agent": "^0.99.2",
51
+ "typescript": "^6.0.2",
52
+ "vitest": "^4.1.2"
53
+ }
54
+ }