@parall/daemon 1.31.0 → 1.32.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.
Files changed (55) hide show
  1. package/bundle/manifest.json +16 -11
  2. package/bundle/package.json +1 -0
  3. package/bundle/parall-claude-agent.js +27610 -375
  4. package/bundle/parall-codex-agent.js +27657 -408
  5. package/bundle/parall-daemon.js +30290 -1778
  6. package/bundle/parall-openclaw-agent.js +49 -24
  7. package/dist/cli.d.ts +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +83 -83
  10. package/dist/clip-runtime/bun-resolver.d.ts +24 -0
  11. package/dist/clip-runtime/bun-resolver.d.ts.map +1 -0
  12. package/dist/clip-runtime/bun-resolver.js +58 -0
  13. package/dist/clip-runtime/clip-installer.d.ts +44 -0
  14. package/dist/clip-runtime/clip-installer.d.ts.map +1 -0
  15. package/dist/clip-runtime/clip-installer.js +542 -0
  16. package/dist/clip-runtime/clip-provider.d.ts +78 -0
  17. package/dist/clip-runtime/clip-provider.d.ts.map +1 -0
  18. package/dist/clip-runtime/clip-provider.js +430 -0
  19. package/dist/clip-runtime/index.d.ts +7 -0
  20. package/dist/clip-runtime/index.d.ts.map +1 -0
  21. package/dist/clip-runtime/index.js +5 -0
  22. package/dist/clip-runtime/ipc.d.ts +94 -0
  23. package/dist/clip-runtime/ipc.d.ts.map +1 -0
  24. package/dist/clip-runtime/ipc.js +98 -0
  25. package/dist/clip-runtime/manifest.d.ts +74 -0
  26. package/dist/clip-runtime/manifest.d.ts.map +1 -0
  27. package/dist/clip-runtime/manifest.js +181 -0
  28. package/dist/clip-runtime/process-manager.d.ts +57 -0
  29. package/dist/clip-runtime/process-manager.d.ts.map +1 -0
  30. package/dist/clip-runtime/process-manager.js +333 -0
  31. package/dist/clip-runtime/process.d.ts +59 -0
  32. package/dist/clip-runtime/process.d.ts.map +1 -0
  33. package/dist/clip-runtime/process.js +350 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +24 -24
  36. package/dist/filesystem.d.ts +1 -1
  37. package/dist/filesystem.d.ts.map +1 -1
  38. package/dist/filesystem.js +51 -53
  39. package/dist/index.js +20 -15
  40. package/dist/runtimes.d.ts +3 -2
  41. package/dist/runtimes.d.ts.map +1 -1
  42. package/dist/runtimes.js +24 -20
  43. package/dist/supervisor.d.ts +9 -4
  44. package/dist/supervisor.d.ts.map +1 -1
  45. package/dist/supervisor.js +244 -76
  46. package/dist/updater-manifest.d.ts +2 -0
  47. package/dist/updater-manifest.d.ts.map +1 -1
  48. package/dist/updater-manifest.js +6 -6
  49. package/dist/updater.d.ts +2 -2
  50. package/dist/updater.d.ts.map +1 -1
  51. package/dist/updater.js +55 -37
  52. package/dist/workspace.d.ts +2 -2
  53. package/dist/workspace.d.ts.map +1 -1
  54. package/dist/workspace.js +112 -112
  55. package/package.json +6 -6
@@ -0,0 +1,74 @@
1
+ import type { IpcManifest } from './ipc.js';
2
+ export interface ClipConfig {
3
+ name: string;
4
+ package?: string;
5
+ version?: string;
6
+ source: string;
7
+ path: string;
8
+ token?: string;
9
+ manifest?: ManifestCache;
10
+ }
11
+ export interface ManifestCache {
12
+ name: string;
13
+ package?: string;
14
+ version?: string;
15
+ domain?: string;
16
+ description?: string;
17
+ commands: string[];
18
+ commandDetails: CommandDetail[];
19
+ hasWeb?: boolean;
20
+ dependencies?: Record<string, {
21
+ package?: string;
22
+ version?: string;
23
+ }>;
24
+ patterns?: string[];
25
+ entities?: Record<string, unknown>;
26
+ }
27
+ export interface CommandDetail {
28
+ name: string;
29
+ description?: string;
30
+ input?: string;
31
+ output?: string;
32
+ }
33
+ export interface ClipJson {
34
+ name?: string;
35
+ version?: string;
36
+ description?: string;
37
+ runtime?: string;
38
+ main?: string;
39
+ web?: string;
40
+ author?: string;
41
+ license?: string;
42
+ repository?: string;
43
+ }
44
+ interface ProjectMetadata {
45
+ package?: string;
46
+ version?: string;
47
+ description?: string;
48
+ domain?: string;
49
+ main?: string;
50
+ web?: string;
51
+ dependencies?: Record<string, {
52
+ package?: string;
53
+ version?: string;
54
+ }>;
55
+ patterns?: string[];
56
+ commands?: CommandDetail[];
57
+ }
58
+ export declare function loadClipJson(dir: string): ClipJson | null;
59
+ export declare function loadProjectMetadata(clip: ClipConfig): ProjectMetadata;
60
+ /**
61
+ * Parse IPC commands in any of the three Pinix-supported formats:
62
+ * 1. string[] → ["add", "remove"]
63
+ * 2. CommandDetail[] → [{ name: "add", description: "..." }]
64
+ * 3. Record<string, object> → { "add": { description: "..." } }
65
+ */
66
+ export declare function parseIpcCommands(data: unknown): CommandDetail[];
67
+ export declare function enrichManifest(clip: ClipConfig, manifest: ManifestCache): ManifestCache;
68
+ /**
69
+ * Build a ManifestCache from IPC register message manifest data.
70
+ */
71
+ export declare function manifestFromIpc(ipcManifest: IpcManifest): ManifestCache;
72
+ export declare function resolveEntrypoint(clip: ClipConfig): string;
73
+ export {};
74
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAUD,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAQzD;AAYD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAkBrE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,CA+C/D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,GAAG,aAAa,CAoBvF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,WAAW,GAAG,aAAa,CAevE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAY1D"}
@@ -0,0 +1,181 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ export function loadClipJson(dir) {
4
+ const filePath = path.join(dir, 'clip.json');
5
+ try {
6
+ const content = fs.readFileSync(filePath, 'utf-8');
7
+ return JSON.parse(content);
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
13
+ function loadPackageJson(dir) {
14
+ const filePath = path.join(dir, 'package.json');
15
+ try {
16
+ const content = fs.readFileSync(filePath, 'utf-8');
17
+ return JSON.parse(content);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function loadProjectMetadata(clip) {
24
+ const meta = {};
25
+ const clipJson = loadClipJson(clip.path);
26
+ if (clipJson) {
27
+ meta.package = clipJson.name;
28
+ meta.version = clipJson.version;
29
+ meta.description = clipJson.description;
30
+ meta.main = clipJson.main;
31
+ meta.web = clipJson.web;
32
+ }
33
+ const pkgJson = loadPackageJson(clip.path);
34
+ if (pkgJson) {
35
+ if (!meta.package)
36
+ meta.package = pkgJson.name;
37
+ if (!meta.version)
38
+ meta.version = pkgJson.version;
39
+ if (!meta.description)
40
+ meta.description = pkgJson.description;
41
+ if (!meta.main)
42
+ meta.main = pkgJson.main;
43
+ }
44
+ return meta;
45
+ }
46
+ /**
47
+ * Parse IPC commands in any of the three Pinix-supported formats:
48
+ * 1. string[] → ["add", "remove"]
49
+ * 2. CommandDetail[] → [{ name: "add", description: "..." }]
50
+ * 3. Record<string, object> → { "add": { description: "..." } }
51
+ */
52
+ export function parseIpcCommands(data) {
53
+ if (!data)
54
+ return [];
55
+ if (Array.isArray(data)) {
56
+ if (data.length === 0)
57
+ return [];
58
+ if (typeof data[0] === 'string') {
59
+ return data.map((name) => ({ name: name.trim() }));
60
+ }
61
+ return data
62
+ .map((item) => {
63
+ const name = (item.name ?? item.command ?? item.id ?? '');
64
+ return {
65
+ name: String(name).trim(),
66
+ description: item.description ? String(item.description).trim() : undefined,
67
+ input: item.input
68
+ ? typeof item.input === 'string'
69
+ ? item.input
70
+ : JSON.stringify(item.input)
71
+ : undefined,
72
+ output: item.output
73
+ ? typeof item.output === 'string'
74
+ ? item.output
75
+ : JSON.stringify(item.output)
76
+ : undefined,
77
+ };
78
+ })
79
+ .filter((c) => c.name);
80
+ }
81
+ if (typeof data === 'object' && data !== null) {
82
+ const record = data;
83
+ return Object.entries(record)
84
+ .map(([name, details]) => ({
85
+ name: name.trim(),
86
+ description: details?.description ? String(details.description).trim() : undefined,
87
+ input: details?.input
88
+ ? typeof details.input === 'string'
89
+ ? details.input
90
+ : JSON.stringify(details.input)
91
+ : undefined,
92
+ output: details?.output
93
+ ? typeof details.output === 'string'
94
+ ? details.output
95
+ : JSON.stringify(details.output)
96
+ : undefined,
97
+ }))
98
+ .filter((c) => c.name);
99
+ }
100
+ return [];
101
+ }
102
+ export function enrichManifest(clip, manifest) {
103
+ const meta = loadProjectMetadata(clip);
104
+ const enriched = { ...manifest };
105
+ if (!enriched.name)
106
+ enriched.name = clip.name;
107
+ if (!enriched.package && meta.package)
108
+ enriched.package = meta.package;
109
+ if (!enriched.version && meta.version)
110
+ enriched.version = meta.version;
111
+ if (!enriched.description && meta.description)
112
+ enriched.description = meta.description;
113
+ if (!enriched.domain && meta.domain)
114
+ enriched.domain = meta.domain;
115
+ if (meta.commands && meta.commands.length > 0 && enriched.commandDetails.length === 0) {
116
+ enriched.commandDetails = meta.commands;
117
+ enriched.commands = meta.commands.map((c) => c.name);
118
+ }
119
+ if (enriched.hasWeb === undefined && meta.web) {
120
+ enriched.hasWeb = true;
121
+ }
122
+ return finalizeManifest(enriched);
123
+ }
124
+ /**
125
+ * Build a ManifestCache from IPC register message manifest data.
126
+ */
127
+ export function manifestFromIpc(ipcManifest) {
128
+ const details = parseIpcCommands(ipcManifest.commands);
129
+ return {
130
+ name: '',
131
+ package: ipcManifest.package,
132
+ version: ipcManifest.version,
133
+ domain: ipcManifest.domain,
134
+ description: ipcManifest.description,
135
+ commands: details.map((c) => c.name),
136
+ commandDetails: details,
137
+ hasWeb: ipcManifest.has_web,
138
+ dependencies: ipcManifest.dependencies,
139
+ patterns: ipcManifest.patterns,
140
+ entities: ipcManifest.entities,
141
+ };
142
+ }
143
+ export function resolveEntrypoint(clip) {
144
+ const clipJson = loadClipJson(clip.path);
145
+ if (clipJson?.main)
146
+ return path.join(clip.path, clipJson.main);
147
+ const pkgJson = loadPackageJson(clip.path);
148
+ if (pkgJson?.main)
149
+ return path.join(clip.path, pkgJson.main);
150
+ if (pkgJson?.bin) {
151
+ const binPath = typeof pkgJson.bin === 'string' ? pkgJson.bin : Object.values(pkgJson.bin)[0];
152
+ if (binPath)
153
+ return path.join(clip.path, binPath);
154
+ }
155
+ const defaultEntry = path.join(clip.path, 'index.ts');
156
+ if (fs.existsSync(defaultEntry))
157
+ return defaultEntry;
158
+ return path.join(clip.path, 'index.js');
159
+ }
160
+ function finalizeManifest(manifest) {
161
+ const m = { ...manifest };
162
+ m.name = (m.name ?? '').trim();
163
+ m.package = m.package?.trim();
164
+ m.version = m.version?.trim();
165
+ m.domain = m.domain?.trim();
166
+ m.description = m.description?.trim();
167
+ if (m.commandDetails.length > 0) {
168
+ const seen = new Set();
169
+ m.commandDetails = m.commandDetails
170
+ .map((c) => ({ ...c, name: c.name.trim(), description: c.description?.trim() }))
171
+ .filter((c) => {
172
+ if (!c.name || seen.has(c.name))
173
+ return false;
174
+ seen.add(c.name);
175
+ return true;
176
+ })
177
+ .sort((a, b) => a.name.localeCompare(b.name));
178
+ m.commands = m.commandDetails.map((c) => c.name);
179
+ }
180
+ return m;
181
+ }
@@ -0,0 +1,57 @@
1
+ import { type ClipProcessStatus } from './process.js';
2
+ import type { ClipConfig, ManifestCache } from './manifest.js';
3
+ export interface ClipProcessManagerOptions {
4
+ bunPath?: string;
5
+ clipsDir: string;
6
+ dataDir: string;
7
+ }
8
+ export declare class ClipProcessManager {
9
+ private bunPath;
10
+ private clipsDir;
11
+ private dataDir;
12
+ private processes;
13
+ private startingUp;
14
+ private statuses;
15
+ private clipConfigs;
16
+ private shuttingDown;
17
+ private statusListeners;
18
+ private manifestListeners;
19
+ constructor(opts: ClipProcessManagerOptions);
20
+ /** Backward-compat: set a single status listener (wraps addStatusListener). */
21
+ set onStatusChange(fn: ((name: string, status: ClipProcessStatus, message: string) => void) | undefined);
22
+ /** Subscribe to clip status changes. Returns an unsubscribe function. */
23
+ addStatusListener(fn: (name: string, status: ClipProcessStatus, message: string) => void): () => void;
24
+ addManifestListener(fn: (clip: ClipConfig) => void): () => void;
25
+ private notifyManifestUpdate;
26
+ private resolveBun;
27
+ registerClip(config: ClipConfig): void;
28
+ unregisterClip(name: string): Promise<void>;
29
+ startClip(name: string): Promise<void>;
30
+ stopClip(name: string): Promise<void>;
31
+ stopAll(): Promise<void>;
32
+ /**
33
+ * Invoke a command on a clip. Auto-starts if sleeping, retries once on process crash.
34
+ * Application-level errors (ClipCommandError) are NOT retried.
35
+ * Corresponds to Pinix ProcessManager.Invoke().
36
+ */
37
+ invoke(name: string, command: string, input?: unknown): Promise<unknown>;
38
+ invokeStream(name: string, command: string, input?: unknown, onChunk?: (chunk: unknown) => void): Promise<unknown>;
39
+ loadManifest(name: string): Promise<ManifestCache>;
40
+ clipStatus(name: string): {
41
+ status: ClipProcessStatus;
42
+ message: string;
43
+ };
44
+ isRunning(name: string): boolean;
45
+ getRegisteredClips(): ClipConfig[];
46
+ /**
47
+ * Load clip configs from the clips directory.
48
+ * Reads clip-config.json if present, or scans subdirectories for clip.json files.
49
+ */
50
+ loadInstalledClips(): Promise<void>;
51
+ private ensureProcess;
52
+ private spawnProcess;
53
+ private ensureClipDataDir;
54
+ private removeIfSame;
55
+ private setStatus;
56
+ }
57
+ //# sourceMappingURL=process-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-manager.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/process-manager.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,iBAAiB,EAEvB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAQ/D,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,UAAU,CAA2C;IAC7D,OAAO,CAAC,QAAQ,CAAsC;IACtD,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,eAAe,CAEhB;IACP,OAAO,CAAC,iBAAiB,CAAyC;gBAEtD,IAAI,EAAE,yBAAyB;IAM3C,+EAA+E;IAC/E,IAAI,cAAc,CAAC,EAAE,EACjB,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GACpE,SAAS,EAQZ;IAED,yEAAyE;IACzE,iBAAiB,CACf,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACrE,MAAM,IAAI;IAQb,mBAAmB,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAQ/D,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,UAAU;IAOlB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAOhC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAItC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAarC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB9B;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBxE,YAAY,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,EACf,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,OAAO,CAAC,OAAO,CAAC;IAgBb,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAOxD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,iBAAiB,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAMxE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKhC,kBAAkB,IAAI,UAAU,EAAE;IAIlC;;;OAGG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;YA0D3B,aAAa;YAwBb,YAAY;IAgD1B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,SAAS;CAUlB"}
@@ -0,0 +1,333 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { ClipProcess, ClipCommandError, } from './process.js';
4
+ import { findBunBinary } from './bun-resolver.js';
5
+ export class ClipProcessManager {
6
+ bunPath;
7
+ clipsDir;
8
+ dataDir;
9
+ processes = new Map();
10
+ startingUp = new Map();
11
+ statuses = new Map();
12
+ clipConfigs = new Map();
13
+ shuttingDown = new Set();
14
+ statusListeners = [];
15
+ manifestListeners = [];
16
+ constructor(opts) {
17
+ this.bunPath = opts.bunPath ?? '';
18
+ this.clipsDir = opts.clipsDir;
19
+ this.dataDir = opts.dataDir;
20
+ }
21
+ /** Backward-compat: set a single status listener (wraps addStatusListener). */
22
+ set onStatusChange(fn) {
23
+ // Remove any previously installed compat listener, then add the new one.
24
+ // We store the unsubscribe handle on the function itself to avoid an extra field.
25
+ const prev = this._compatUnsub;
26
+ if (prev)
27
+ prev();
28
+ if (fn) {
29
+ this._compatUnsub = this.addStatusListener(fn);
30
+ }
31
+ }
32
+ /** Subscribe to clip status changes. Returns an unsubscribe function. */
33
+ addStatusListener(fn) {
34
+ this.statusListeners.push(fn);
35
+ return () => {
36
+ const idx = this.statusListeners.indexOf(fn);
37
+ if (idx >= 0)
38
+ this.statusListeners.splice(idx, 1);
39
+ };
40
+ }
41
+ addManifestListener(fn) {
42
+ this.manifestListeners.push(fn);
43
+ return () => {
44
+ const idx = this.manifestListeners.indexOf(fn);
45
+ if (idx >= 0)
46
+ this.manifestListeners.splice(idx, 1);
47
+ };
48
+ }
49
+ notifyManifestUpdate(clip) {
50
+ for (const fn of this.manifestListeners) {
51
+ try {
52
+ fn(clip);
53
+ }
54
+ catch {
55
+ /* listener error should not break spawn */
56
+ }
57
+ }
58
+ }
59
+ resolveBun() {
60
+ if (!this.bunPath) {
61
+ this.bunPath = findBunBinary();
62
+ }
63
+ return this.bunPath;
64
+ }
65
+ registerClip(config) {
66
+ this.clipConfigs.set(config.name, config);
67
+ if (!this.statuses.has(config.name)) {
68
+ this.setStatus(config.name, 'sleeping', 'registered');
69
+ }
70
+ }
71
+ async unregisterClip(name) {
72
+ this.shuttingDown.add(name);
73
+ try {
74
+ const startup = this.startingUp.get(name);
75
+ if (startup) {
76
+ this.startingUp.delete(name);
77
+ await startup.then((proc) => proc.stop()).catch(() => { });
78
+ }
79
+ if (this.processes.has(name)) {
80
+ await this.stopClip(name);
81
+ }
82
+ this.clipConfigs.delete(name);
83
+ this.statuses.delete(name);
84
+ }
85
+ finally {
86
+ this.shuttingDown.delete(name);
87
+ }
88
+ }
89
+ async startClip(name) {
90
+ await this.ensureProcess(name);
91
+ }
92
+ async stopClip(name) {
93
+ this.shuttingDown.add(name);
94
+ try {
95
+ const proc = this.processes.get(name);
96
+ if (!proc)
97
+ return;
98
+ await proc.stop();
99
+ this.processes.delete(name);
100
+ this.setStatus(name, 'sleeping', 'stopped');
101
+ }
102
+ finally {
103
+ this.shuttingDown.delete(name);
104
+ }
105
+ }
106
+ async stopAll() {
107
+ // Mark all clips as shutting down before any async work
108
+ for (const name of this.processes.keys())
109
+ this.shuttingDown.add(name);
110
+ for (const name of this.startingUp.keys())
111
+ this.shuttingDown.add(name);
112
+ const stops = Array.from(this.processes.keys()).map((name) => this.stopClip(name));
113
+ // Also abort any in-flight startups (register window)
114
+ for (const [, startup] of this.startingUp) {
115
+ stops.push(startup.then((proc) => proc.stop()).catch(() => { }));
116
+ }
117
+ this.startingUp.clear();
118
+ await Promise.allSettled(stops);
119
+ // shuttingDown is cleared by individual stopClip finally blocks;
120
+ // clear any remaining entries from startingUp-only clips.
121
+ this.shuttingDown.clear();
122
+ }
123
+ /**
124
+ * Invoke a command on a clip. Auto-starts if sleeping, retries once on process crash.
125
+ * Application-level errors (ClipCommandError) are NOT retried.
126
+ * Corresponds to Pinix ProcessManager.Invoke().
127
+ */
128
+ async invoke(name, command, input) {
129
+ try {
130
+ const proc = await this.ensureProcess(name);
131
+ const result = await proc.invoke(command, input);
132
+ return result.output;
133
+ }
134
+ catch (err) {
135
+ if (err instanceof ClipCommandError)
136
+ throw err;
137
+ const old = this.processes.get(name);
138
+ if (old?.alive())
139
+ await old.stop().catch(() => { });
140
+ this.processes.delete(name);
141
+ const proc = await this.ensureProcess(name);
142
+ const result = await proc.invoke(command, input);
143
+ return result.output;
144
+ }
145
+ }
146
+ async invokeStream(name, command, input, onChunk) {
147
+ try {
148
+ const proc = await this.ensureProcess(name);
149
+ const result = await proc.invokeStream(command, input, onChunk);
150
+ return result.output;
151
+ }
152
+ catch (err) {
153
+ if (err instanceof ClipCommandError)
154
+ throw err;
155
+ const old = this.processes.get(name);
156
+ if (old?.alive())
157
+ await old.stop().catch(() => { });
158
+ this.processes.delete(name);
159
+ const proc = await this.ensureProcess(name);
160
+ const result = await proc.invokeStream(command, input, onChunk);
161
+ return result.output;
162
+ }
163
+ }
164
+ async loadManifest(name) {
165
+ const proc = await this.ensureProcess(name);
166
+ const manifest = proc.getManifest();
167
+ if (!manifest)
168
+ throw new Error(`clip "${name}" has no manifest`);
169
+ return manifest;
170
+ }
171
+ clipStatus(name) {
172
+ const entry = this.statuses.get(name);
173
+ if (!entry)
174
+ return { status: 'sleeping', message: 'unknown' };
175
+ return { status: entry.status, message: entry.message };
176
+ }
177
+ isRunning(name) {
178
+ const proc = this.processes.get(name);
179
+ return proc !== undefined && proc.alive();
180
+ }
181
+ getRegisteredClips() {
182
+ return Array.from(this.clipConfigs.values());
183
+ }
184
+ /**
185
+ * Load clip configs from the clips directory.
186
+ * Reads clip-config.json if present, or scans subdirectories for clip.json files.
187
+ */
188
+ async loadInstalledClips() {
189
+ const configFile = path.join(path.dirname(this.clipsDir), 'clip-config.json');
190
+ if (fs.existsSync(configFile)) {
191
+ try {
192
+ const content = fs.readFileSync(configFile, 'utf-8');
193
+ const configs = JSON.parse(content);
194
+ for (const config of configs) {
195
+ this.registerClip(config);
196
+ }
197
+ return;
198
+ }
199
+ catch {
200
+ console.warn('[clip-manager] failed to parse clip-config.json, scanning directory');
201
+ }
202
+ }
203
+ if (!fs.existsSync(this.clipsDir))
204
+ return;
205
+ const entries = fs.readdirSync(this.clipsDir, { withFileTypes: true });
206
+ for (const entry of entries) {
207
+ if (!entry.isDirectory())
208
+ continue;
209
+ const clipDir = path.join(this.clipsDir, entry.name);
210
+ // Try clip.json first, fall back to package.json
211
+ const clipJsonPath = path.join(clipDir, 'clip.json');
212
+ const pkgJsonPath = path.join(clipDir, 'package.json');
213
+ if (fs.existsSync(clipJsonPath)) {
214
+ try {
215
+ const clipJson = JSON.parse(fs.readFileSync(clipJsonPath, 'utf-8'));
216
+ this.registerClip({
217
+ name: clipJson.name || entry.name,
218
+ package: clipJson.name,
219
+ version: clipJson.version,
220
+ source: clipDir,
221
+ path: clipDir,
222
+ });
223
+ }
224
+ catch {
225
+ console.warn(`[clip-manager] failed to parse clip.json in ${clipDir}`);
226
+ }
227
+ }
228
+ else if (fs.existsSync(pkgJsonPath)) {
229
+ try {
230
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
231
+ this.registerClip({
232
+ name: pkgJson.name || entry.name,
233
+ package: pkgJson.name,
234
+ version: pkgJson.version,
235
+ source: clipDir,
236
+ path: clipDir,
237
+ });
238
+ }
239
+ catch {
240
+ console.warn(`[clip-manager] failed to parse package.json in ${clipDir}`);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ // --- internal ---
246
+ async ensureProcess(name) {
247
+ if (this.shuttingDown.has(name)) {
248
+ throw new Error(`clip "${name}" is shutting down`);
249
+ }
250
+ const existing = this.processes.get(name);
251
+ if (existing && existing.alive())
252
+ return existing;
253
+ // Deduplicate concurrent cold-start requests
254
+ const inflight = this.startingUp.get(name);
255
+ if (inflight)
256
+ return inflight;
257
+ const config = this.clipConfigs.get(name);
258
+ if (!config)
259
+ throw new Error(`clip "${name}" not registered`);
260
+ const startup = this.spawnProcess(config);
261
+ this.startingUp.set(name, startup);
262
+ try {
263
+ return await startup;
264
+ }
265
+ finally {
266
+ this.startingUp.delete(name);
267
+ }
268
+ }
269
+ async spawnProcess(config) {
270
+ const proc = new ClipProcess(config);
271
+ const clipDataDir = this.ensureClipDataDir(config);
272
+ const bunPath = this.resolveBun();
273
+ proc.spawn(bunPath, {
274
+ PINIX_DATA_DIR: clipDataDir,
275
+ // Expose the embedded bun's directory on the clip's PATH so a clip that
276
+ // shells out to `bun` by name resolves it — the daemon's launchd PATH
277
+ // does not include Frameworks/. Symmetric with installDeps(); relies on
278
+ // the embedded binary being named `bun` (Frameworks/bun).
279
+ PATH: `${path.dirname(bunPath)}${path.delimiter}${process.env.PATH ?? ''}`,
280
+ });
281
+ this.setStatus(config.name, 'running', 'starting');
282
+ try {
283
+ await proc.waitReady();
284
+ // Only expose to concurrent callers after registration completes
285
+ this.processes.set(config.name, proc);
286
+ this.setStatus(config.name, 'running', 'ready');
287
+ // Always cache manifest so Provider registrations have commands
288
+ const manifest = proc.getManifest();
289
+ if (manifest) {
290
+ config.manifest = manifest;
291
+ this.notifyManifestUpdate(config);
292
+ }
293
+ }
294
+ catch (err) {
295
+ const message = err instanceof Error ? err.message : String(err);
296
+ this.setStatus(config.name, 'error', message);
297
+ throw err;
298
+ }
299
+ // Monitor process exit in background
300
+ proc.waitDone().then(() => {
301
+ this.removeIfSame(config.name, proc);
302
+ const exitErr = proc.getError();
303
+ if (exitErr) {
304
+ this.setStatus(config.name, 'error', exitErr.message);
305
+ }
306
+ else {
307
+ this.setStatus(config.name, 'sleeping', 'exited');
308
+ }
309
+ });
310
+ return proc;
311
+ }
312
+ ensureClipDataDir(config) {
313
+ const dir = path.join(this.dataDir, config.name);
314
+ fs.mkdirSync(dir, { recursive: true });
315
+ return dir;
316
+ }
317
+ removeIfSame(name, proc) {
318
+ if (this.processes.get(name) === proc) {
319
+ this.processes.delete(name);
320
+ }
321
+ }
322
+ setStatus(name, status, message) {
323
+ this.statuses.set(name, { status, message, since: new Date() });
324
+ for (const fn of this.statusListeners) {
325
+ try {
326
+ fn(name, status, message);
327
+ }
328
+ catch {
329
+ /* listener must not break status propagation */
330
+ }
331
+ }
332
+ }
333
+ }