@pimote/sdk 0.14.0 → 0.15.0-rc.1

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 CHANGED
@@ -3,9 +3,10 @@
3
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
4
 
5
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
6
+ - **`@pimote/sdk/folders`** — contribute folder discovery and creation to pimote's folder layer
7
+ - **`@pimote/sdk/activity`** — answer pimote's idle-reap background-activity poll
7
8
 
8
- The root `@pimote/sdk` import re-exports both modules.
9
+ The root `@pimote/sdk` import re-exports all modules.
9
10
 
10
11
  ## Install
11
12
 
@@ -13,7 +14,7 @@ The root `@pimote/sdk` import re-exports both modules.
13
14
  npm install @pimote/sdk
14
15
  ```
15
16
 
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
+ Requires `@earendil-works/pi-coding-agent` as an optional peer dependency (already present in any pi extension; only the panels and activity seams import from it).
17
18
 
18
19
  ## Panels — `@pimote/sdk/panels`
19
20
 
@@ -111,17 +112,17 @@ Detection uses pi's EventBus for a synchronous in-process round-trip — pimote'
111
112
 
112
113
  When pimote isn't present, the EventBus emit fires with no listener, `detect()` returns `null`, and there's zero overhead.
113
114
 
114
- ## Projects — `@pimote/sdk/projects`
115
+ ## Folders — `@pimote/sdk/folders`
115
116
 
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
+ Type-only module for user-authored folder-source modules: pimote scans a folder-sources directory (default `~/.config/pimote/folder-sources`, configurable via `folderSourcesDir`), dynamic-imports every JS/TS module it finds, and collects each module's exported `sources` and `creators` arrays. Sources contribute repos and hub folders to pimote's folder layer; creators make new folders from user-supplied form params.
117
118
 
118
119
  ### Writing a module
119
120
 
120
121
  ```ts
121
- // ~/.config/pimote/project-sources/my-source.ts
122
- import type { ProjectSource, ProjectCreator } from '@pimote/sdk/projects';
122
+ // ~/.config/pimote/folder-sources/my-source.ts
123
+ import type { FolderSource, FolderCreator } from '@pimote/sdk/folders';
123
124
 
124
- export const sources: ProjectSource[] = [
125
+ export const sources: FolderSource[] = [
125
126
  {
126
127
  id: 'my-source',
127
128
  async list() {
@@ -140,42 +141,42 @@ export const sources: ProjectSource[] = [
140
141
  },
141
142
  ];
142
143
 
143
- export const creators: ProjectCreator[] = [
144
+ export const creators: FolderCreator[] = [
144
145
  {
145
146
  id: 'my-creator',
146
147
  describe() {
147
- return { label: 'New API project', paramSchema: { name: 'string', tags: 'string[]' } };
148
+ return { label: 'New API folder', paramSchema: { name: 'string', tags: 'string[]' } };
148
149
  },
149
150
  async create(params) {
150
151
  const path = `/home/me/work/${String(params.name)}`;
151
- // ... scaffold the project directory ...
152
+ // ... scaffold the folder ...
152
153
  return { path };
153
154
  },
154
155
  },
155
156
  ];
156
157
  ```
157
158
 
158
- ### `ProjectSource`
159
+ ### `FolderSource`
159
160
 
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
+ The discovery seam. `list()` is called on cache miss and must return the current entries without mutating anything. The optional `onFolderOpen(folderPath)` 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
 
162
- ### `ProjectCreator`
163
+ ### `FolderCreator`
163
164
 
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
+ 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 folder, and returns its `{ path }`.
165
166
 
166
167
  ### Entry shapes
167
168
 
168
- `list()` returns `SourceEntry[]` — a repo or a multi-repo project:
169
+ `list()` returns `SourceEntry[]` — a repo or a hub folder:
169
170
 
170
171
  ```ts
171
- /** Feeds a single-repo project. Extends `RepoInfo`. */
172
+ /** Feeds a single-repo folder. Extends `RepoInfo`. */
172
173
  interface RepoSourceEntry extends RepoInfo {
173
174
  kind: 'repo';
174
175
  }
175
176
 
176
- /** Feeds a multi-repo project over member paths. */
177
- interface MultiRepoSourceEntry {
178
- kind: 'project';
177
+ /** Feeds a hub folder over member paths. */
178
+ interface HubSourceEntry {
179
+ kind: 'hub';
179
180
  path: string;
180
181
  name: string;
181
182
  memberPaths: string[];
@@ -188,6 +189,61 @@ interface MultiRepoSourceEntry {
188
189
 
189
190
  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
 
192
+ ## Activity — `@pimote/sdk/activity`
193
+
194
+ Answer pimote's background-activity poll so the idle reaper does not close a session that holds live work (for example: background subagents still running while the root session is settled).
195
+
196
+ Pimote reaps sessions that are idle past a timeout with no connected client. Before closing, it emits `pimote:activity:request` on the session's EventBus and reads the answer synchronously. Register a responder if your extension manages work the root session cannot see:
197
+
198
+ ```ts
199
+ import { answerActivity } from '@pimote/sdk/activity';
200
+ import type { ExtensionFactory } from '@earendil-works/pi-coding-agent';
201
+
202
+ const extension: ExtensionFactory = (pi) => {
203
+ answerActivity(pi, () => myAgents.some((a) => a.state === 'running'));
204
+ };
205
+
206
+ export default extension;
207
+ ```
208
+
209
+ The callback must be synchronous — the round-trip is a plain inline emit, so an async answer is indistinguishable from no answer. It is read at poll time, not registration time, so it may consult live state through a stable owner object. If it throws, the responder answers `active: true`: a broken predicate parks the session (recoverable by explicit close) instead of destroying work.
210
+
211
+ What counts as "live work" is extension policy — pimote learns only the boolean. With no responder, pimote reaps as it always did.
212
+
213
+ ### API
214
+
215
+ #### `answerActivity(pi, isActive)`
216
+
217
+ Registers the responder on the session's EventBus. Returns an unsubscribe function (for symmetry and tests — the bus dies with the session).
218
+
219
+ - **`pi`** — The `ExtensionAPI` object passed to your extension factory.
220
+ - **`isActive`** — Synchronous `() => boolean`: true when closing the session now would destroy live work.
221
+
222
+ #### `askActivity(bus)`
223
+
224
+ The pimote-side twin, used by the server's idle reaper. Performs the synchronous poll on an `EventBus` and returns whether any responder asserted live work (multiple answers are OR-ed). Extensions do not need it; it ships in the SDK so the whole protocol lives in one module.
225
+
226
+ ## Breaking changes
227
+
228
+ ### 0.15.0 (0.x breaking)
229
+
230
+ The discovery/creation seam is renamed to folder vocabulary — "project" is gone from the SDK surface. No compatibility type aliases exist; source modules must be updated against these successors:
231
+
232
+ | Retired name (≤0.14.x) | Replacement (0.15.0) |
233
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
234
+ | `@pimote/sdk/projects` subpath | `@pimote/sdk/folders` |
235
+ | `ProjectSource` | `FolderSource` |
236
+ | `ProjectCreator` | `FolderCreator` |
237
+ | `ProjectCreatorDescriptor` | `FolderCreatorDescriptor` |
238
+ | `ProjectCreatorParamType` | `FolderCreatorParamType` |
239
+ | `MultiRepoSourceEntry` | `HubSourceEntry` |
240
+ | entry discriminator `kind: 'project'` | entry discriminator `kind: 'hub'` |
241
+ | `onProjectOpen(projectPath)` | `onFolderOpen(folderPath)` |
242
+ | config key `projectSourcesDir` | `folderSourcesDir` (legacy key still read as a deprecated fallback) |
243
+ | sources dir `~/.config/pimote/project-sources` | `~/.config/pimote/folder-sources` (legacy dir still loaded when the new one is absent) |
244
+
245
+ Every name in the left column is retired; each is listed here only so existing modules can be migrated. The wire command for the built-in creation flow is likewise renamed (`create_project` → `create_folder`).
246
+
191
247
  ## License
192
248
 
193
249
  MIT
@@ -0,0 +1,20 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ /**
3
+ * Answer pimote's background-activity polls for this session.
4
+ *
5
+ * Pimote's idle reaper calls this poll (DR-054) before closing an idle session
6
+ * with no connected client. Return true when closing the session now would
7
+ * destroy live work (for example: background subagents still running).
8
+ *
9
+ * The poll is a synchronous EventBus round-trip, mirroring `panels/detect`:
10
+ * pimote emits `pimote:activity:request` and reads the response inline. The
11
+ * `isActive` callback must therefore be synchronous — an async answer is
12
+ * indistinguishable from no answer. Answering is extension policy: pimote
13
+ * learns only the boolean, never what the work is.
14
+ *
15
+ * If `isActive` throws, the responder answers `active: true`: a broken
16
+ * predicate must park the session (recoverable by explicit close), not destroy
17
+ * work. Register once per session; the returned unsubscribe is for symmetry
18
+ * and tests — the bus dies with the session.
19
+ */
20
+ export declare function answerActivity(pi: ExtensionAPI, isActive: () => boolean): () => void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Answer pimote's background-activity polls for this session.
3
+ *
4
+ * Pimote's idle reaper calls this poll (DR-054) before closing an idle session
5
+ * with no connected client. Return true when closing the session now would
6
+ * destroy live work (for example: background subagents still running).
7
+ *
8
+ * The poll is a synchronous EventBus round-trip, mirroring `panels/detect`:
9
+ * pimote emits `pimote:activity:request` and reads the response inline. The
10
+ * `isActive` callback must therefore be synchronous — an async answer is
11
+ * indistinguishable from no answer. Answering is extension policy: pimote
12
+ * learns only the boolean, never what the work is.
13
+ *
14
+ * If `isActive` throws, the responder answers `active: true`: a broken
15
+ * predicate must park the session (recoverable by explicit close), not destroy
16
+ * work. Register once per session; the returned unsubscribe is for symmetry
17
+ * and tests — the bus dies with the session.
18
+ */
19
+ export function answerActivity(pi, isActive) {
20
+ return pi.events.on('pimote:activity:request', () => {
21
+ let active;
22
+ try {
23
+ active = isActive();
24
+ }
25
+ catch (error) {
26
+ console.error('[pimote:activity] responder threw; answering active to protect live work', error);
27
+ active = true;
28
+ }
29
+ pi.events.emit('pimote:activity:response', { active });
30
+ });
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { EventBus } from '@earendil-works/pi-coding-agent';
2
+ /**
3
+ * Poll a session's extensions for background activity (DR-054).
4
+ *
5
+ * Synchronous round-trip, mirroring `panels/detect`: responders run inline
6
+ * during `emit`, so their answers are captured before this returns. Call only
7
+ * for sessions already past the idle-reap threshold — the poll is invisible
8
+ * until it matters.
9
+ *
10
+ * Fails open: no responder, or any responder answering `active: false`, means
11
+ * no background work and the caller may reap. When several extensions answer,
12
+ * responses are OR-ed — any assertion of live work holds the session.
13
+ */
14
+ export declare function askActivity(bus: EventBus): boolean;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Poll a session's extensions for background activity (DR-054).
3
+ *
4
+ * Synchronous round-trip, mirroring `panels/detect`: responders run inline
5
+ * during `emit`, so their answers are captured before this returns. Call only
6
+ * for sessions already past the idle-reap threshold — the poll is invisible
7
+ * until it matters.
8
+ *
9
+ * Fails open: no responder, or any responder answering `active: false`, means
10
+ * no background work and the caller may reap. When several extensions answer,
11
+ * responses are OR-ed — any assertion of live work holds the session.
12
+ */
13
+ export function askActivity(bus) {
14
+ let active = false;
15
+ const unsubscribe = bus.on('pimote:activity:response', (data) => {
16
+ const message = data;
17
+ if (message?.active === true)
18
+ active = true;
19
+ });
20
+ bus.emit('pimote:activity:request', {});
21
+ unsubscribe();
22
+ return active;
23
+ }
@@ -0,0 +1,3 @@
1
+ export type { ActivityMessage } from './types.js';
2
+ export { answerActivity } from './answer.js';
3
+ export { askActivity } from './ask.js';
@@ -0,0 +1,2 @@
1
+ export { answerActivity } from './answer.js';
2
+ export { askActivity } from './ask.js';
@@ -0,0 +1,5 @@
1
+ /** Response payload for pimote's background-activity poll. */
2
+ export interface ActivityMessage {
3
+ /** True when the responder asserts that closing the session would destroy live work. */
4
+ active: boolean;
5
+ }
@@ -0,0 +1 @@
1
+ export type { FolderSource, FolderCreator, FolderCreatorDescriptor, FolderCreatorParamType, SourceEntry, RepoSourceEntry, HubSourceEntry, RepoInfo } from './types.js';
@@ -18,33 +18,33 @@ export interface RepoInfo {
18
18
  tags?: string[];
19
19
  }
20
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). */
21
+ export type FolderCreatorParamType = 'string' | 'string[]';
22
+ /** A repo entry contributed to the discovery index (feeds single-repo folders). */
23
23
  export interface RepoSourceEntry extends RepoInfo {
24
24
  kind: 'repo';
25
25
  }
26
26
  /**
27
- * A multi-repo project entry contributed to the project layer. `memberPaths`
27
+ * A hub folder entry contributed to the folder layer. `memberPaths`
28
28
  * are concrete paths that may not exist on disk yet; the server derives
29
29
  * existence state itself — sources never flag "virtual".
30
30
  */
31
- export interface MultiRepoSourceEntry {
32
- kind: 'project';
31
+ export interface HubSourceEntry {
32
+ kind: 'hub';
33
33
  path: string;
34
34
  name: string;
35
35
  memberPaths: string[];
36
36
  /** Source-contributed tags; shown to the user but not user-removable. */
37
37
  tags?: string[];
38
38
  }
39
- /** Anything a source lists: a repo, or a multi-repo project over member paths. */
40
- export type SourceEntry = RepoSourceEntry | MultiRepoSourceEntry;
39
+ /** Anything a source lists: a repo, or a hub folder over member paths. */
40
+ export type SourceEntry = RepoSourceEntry | HubSourceEntry;
41
41
  /**
42
- * Extensible discovery seam: a source contributes repos and multi-repo
43
- * projects to the project layer. Built in: the filesystem walker over
42
+ * Extensible discovery seam: a source contributes repos and hub
43
+ * folders to the folder layer. Built in: the filesystem walker over
44
44
  * configured roots. User-authored sources are dynamic-imported from the
45
- * configured project-sources directory.
45
+ * configured folder-sources directory.
46
46
  */
47
- export interface ProjectSource {
47
+ export interface FolderSource {
48
48
  readonly id: string;
49
49
  /** Discover entries; called on cache miss. Must not mutate anything. */
50
50
  list(): Promise<SourceEntry[]>;
@@ -55,22 +55,22 @@ export interface ProjectSource {
55
55
  * if the entry is theirs and missing; do other open-time work otherwise.
56
56
  * A thrown error aborts the open and surfaces the message to the user.
57
57
  */
58
- onProjectOpen?(projectPath: string): Promise<void>;
58
+ onFolderOpen?(folderPath: string): Promise<void>;
59
59
  }
60
60
  /** Human-readable description of a creator's form, for the creation UI. */
61
- export interface ProjectCreatorDescriptor {
61
+ export interface FolderCreatorDescriptor {
62
62
  label: string;
63
- paramSchema: Record<string, ProjectCreatorParamType>;
63
+ paramSchema: Record<string, FolderCreatorParamType>;
64
64
  }
65
65
  /**
66
- * Extensible creation seam: a creator makes a new project directory from
66
+ * Extensible creation seam: a creator makes a new folder from
67
67
  * user-supplied params. Built in: mkdir + git init. User-authored creators
68
- * are dynamic-imported from the configured project-sources directory.
68
+ * are dynamic-imported from the configured folder-sources directory.
69
69
  */
70
- export interface ProjectCreator {
70
+ export interface FolderCreator {
71
71
  readonly id: string;
72
72
  /** Human-readable description for the creation UI. */
73
- describe(): ProjectCreatorDescriptor;
73
+ describe(): FolderCreatorDescriptor;
74
74
  create(params: Record<string, unknown>): Promise<{
75
75
  path: string;
76
76
  }>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './panels/index.js';
2
- export * from './projects/index.js';
2
+ export * from './folders/index.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './panels/index.js';
2
- export * from './projects/index.js';
2
+ export * from './folders/index.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pimote/sdk",
3
- "version": "0.14.0",
4
- "description": "Pimote extensibility SDK: panel cards and project discovery/creation seams for pi extensions",
3
+ "version": "0.15.0-rc.1",
4
+ "description": "Pimote extensibility SDK: panel cards, background-activity reap guard, and folder discovery/creation seams for pi extensions",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Antoine Lennartz <alennartz@gmail.com>",
@@ -17,14 +17,16 @@
17
17
  "pimote",
18
18
  "extension",
19
19
  "sdk",
20
- "projects",
20
+ "folders",
21
21
  "panels",
22
- "cards"
22
+ "cards",
23
+ "activity"
23
24
  ],
24
25
  "exports": {
25
26
  ".": "./dist/index.js",
26
27
  "./panels": "./dist/panels/index.js",
27
- "./projects": "./dist/projects/index.js"
28
+ "./activity": "./dist/activity/index.js",
29
+ "./folders": "./dist/folders/index.js"
28
30
  },
29
31
  "types": "./dist/index.d.ts",
30
32
  "files": [
@@ -39,7 +41,7 @@
39
41
  "prepublishOnly": "npm run build"
40
42
  },
41
43
  "peerDependencies": {
42
- "@earendil-works/pi-coding-agent": "^0.99.2"
44
+ "@earendil-works/pi-coding-agent": "*"
43
45
  },
44
46
  "peerDependenciesMeta": {
45
47
  "@earendil-works/pi-coding-agent": {
@@ -47,7 +49,7 @@
47
49
  }
48
50
  },
49
51
  "devDependencies": {
50
- "@earendil-works/pi-coding-agent": "^0.99.2",
52
+ "@earendil-works/pi-coding-agent": "^1.1.0",
51
53
  "typescript": "^6.0.2",
52
54
  "vitest": "^4.1.2"
53
55
  }
@@ -1 +0,0 @@
1
- export type { ProjectSource, ProjectCreator, ProjectCreatorDescriptor, ProjectCreatorParamType, SourceEntry, RepoSourceEntry, MultiRepoSourceEntry, RepoInfo } from './types.js';
File without changes
File without changes