hotmilk 0.1.9 → 0.1.11
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/AGENTS.md +1 -1
- package/README.md +38 -23
- package/hotmilk.json +3 -2
- package/package.json +5 -3
- package/src/bootstrap/context-stack.ts +3 -11
- package/src/bootstrap/dashboard.ts +225 -46
- package/src/bootstrap/extensions.ts +56 -33
- package/src/bootstrap/global-extension-sources.ts +154 -0
- package/src/bootstrap/resolve-bundled.ts +55 -0
- package/src/bootstrap/session.ts +15 -0
- package/src/config/bundled-extensions.ts +235 -0
- package/src/config/bundled-package-registry.ts +28 -0
- package/src/config/hotmilk.ts +2 -25
- package/src/config/runtime.ts +4 -0
- package/src/controller/input.ts +1 -2
- package/src/controller/mode.ts +2 -3
- package/src/index.ts +6 -6
- package/src/ui/footer.ts +83 -12
- package/src/ui/github-user.ts +153 -0
- package/src/ui/session-logo.ts +9 -4
- package/src/bootstrap/rtk-coexist.ts +0 -6
- package/src/config/extension-groups.ts +0 -32
- package/src/controller/context.ts +0 -5
- package/src/ui/footer-time.ts +0 -9
- package/src/ui/logo.ts +0 -7
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
detectGlobalProviderForBundledExtension,
|
|
6
|
+
HOTMILK_PACKAGE_NAME,
|
|
7
|
+
} from "../config/bundled-package-registry.ts";
|
|
8
|
+
import { BUNDLED_EXTENSION_IDS, type BundledExtensionId } from "../config/hotmilk.ts";
|
|
9
|
+
|
|
10
|
+
const PI_PROJECT_CONFIG_DIR = ".pi";
|
|
11
|
+
|
|
12
|
+
export type GlobalBundledExtensionSkip = {
|
|
13
|
+
id: BundledExtensionId;
|
|
14
|
+
packageName: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CollectGlobalExtensionSourcesOptions = {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
homedir?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isLocalPackageEntry(entry: string): boolean {
|
|
23
|
+
return entry.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(entry);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readPackageName(dir: string): string | null {
|
|
27
|
+
try {
|
|
28
|
+
const pkgPath = path.join(dir, "package.json");
|
|
29
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
30
|
+
const parsed = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { name?: unknown };
|
|
31
|
+
return typeof parsed.name === "string" ? parsed.name : null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolvePackageNameFromLocalPath(entry: string, baseDir: string): string | null {
|
|
38
|
+
const resolved = path.isAbsolute(entry) ? entry : path.resolve(baseDir, entry);
|
|
39
|
+
if (!fs.existsSync(resolved)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let dir = resolved;
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
46
|
+
dir = path.dirname(resolved);
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
while (true) {
|
|
53
|
+
const name = readPackageName(dir);
|
|
54
|
+
if (name) {
|
|
55
|
+
return name;
|
|
56
|
+
}
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Parse `npm:@scope/pkg@1.2.3` → `@scope/pkg`, or `npm:pkg` → `pkg`. */
|
|
66
|
+
export function parseNpmPackageName(entry: string): string | null {
|
|
67
|
+
const trimmed = entry.trim();
|
|
68
|
+
if (!trimmed.startsWith("npm:")) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const spec = trimmed.slice("npm:".length).trim();
|
|
73
|
+
if (!spec) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const versionAt = spec.startsWith("@") ? spec.indexOf("@", 1) : spec.indexOf("@");
|
|
78
|
+
return versionAt === -1 ? spec : spec.slice(0, versionAt);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toStringArray(value: unknown): string[] {
|
|
82
|
+
return Array.isArray(value)
|
|
83
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
84
|
+
: [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readSettingsEntries(settingsPath: string): string[] {
|
|
88
|
+
if (!fs.existsSync(settingsPath)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const raw = fs.readFileSync(settingsPath, "utf-8").trim();
|
|
94
|
+
if (!raw) return [];
|
|
95
|
+
const settings = JSON.parse(raw) as Record<string, unknown>;
|
|
96
|
+
return [...toStringArray(settings.packages), ...toStringArray(settings.extensions)];
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Collect npm package names referenced by Pi settings (global + project). */
|
|
103
|
+
export function collectInstalledPackageNamesFromPiSettings(
|
|
104
|
+
options: CollectGlobalExtensionSourcesOptions = {},
|
|
105
|
+
): Set<string> {
|
|
106
|
+
const home = options.homedir ?? process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
107
|
+
const cwd = options.cwd ?? process.cwd();
|
|
108
|
+
const names = new Set<string>();
|
|
109
|
+
|
|
110
|
+
const sources: Array<{ settingsPath: string; baseDir: string }> = [
|
|
111
|
+
{
|
|
112
|
+
settingsPath: path.join(home, ".pi", "agent", "settings.json"),
|
|
113
|
+
baseDir: path.join(home, ".pi", "agent"),
|
|
114
|
+
},
|
|
115
|
+
{ settingsPath: path.join(cwd, PI_PROJECT_CONFIG_DIR, "settings.json"), baseDir: cwd },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const { settingsPath, baseDir } of sources) {
|
|
119
|
+
for (const entry of readSettingsEntries(settingsPath)) {
|
|
120
|
+
const npmName = parseNpmPackageName(entry);
|
|
121
|
+
if (npmName) {
|
|
122
|
+
names.add(npmName);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isLocalPackageEntry(entry)) {
|
|
127
|
+
const localName = resolvePackageNameFromLocalPath(entry, baseDir);
|
|
128
|
+
if (localName) {
|
|
129
|
+
names.add(localName);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
names.delete(HOTMILK_PACKAGE_NAME);
|
|
136
|
+
return names;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Bundled ids that should not register because Pi settings already provide the package. */
|
|
140
|
+
export function detectGlobalBundledExtensionSkips(
|
|
141
|
+
options: CollectGlobalExtensionSourcesOptions = {},
|
|
142
|
+
): GlobalBundledExtensionSkip[] {
|
|
143
|
+
const installed = collectInstalledPackageNamesFromPiSettings(options);
|
|
144
|
+
const skips: GlobalBundledExtensionSkip[] = [];
|
|
145
|
+
|
|
146
|
+
for (const id of BUNDLED_EXTENSION_IDS) {
|
|
147
|
+
const packageName = detectGlobalProviderForBundledExtension(id, installed);
|
|
148
|
+
if (packageName) {
|
|
149
|
+
skips.push({ id, packageName });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return skips;
|
|
154
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/** Split `pkg/subpath` — supports scoped packages (`@scope/name/...`). */
|
|
6
|
+
export function parseBundledModulePath(relativePath: string): { pkgName: string; subpath: string } {
|
|
7
|
+
if (relativePath.startsWith("@")) {
|
|
8
|
+
const parts = relativePath.split("/");
|
|
9
|
+
return { pkgName: `${parts[0]}/${parts[1]}`, subpath: parts.slice(2).join("/") };
|
|
10
|
+
}
|
|
11
|
+
const slash = relativePath.indexOf("/");
|
|
12
|
+
if (slash === -1) {
|
|
13
|
+
return { pkgName: relativePath, subpath: "index.ts" };
|
|
14
|
+
}
|
|
15
|
+
return { pkgName: relativePath.slice(0, slash), subpath: relativePath.slice(slash + 1) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a bundled dependency file whether npm nested it under hotmilk or hoisted
|
|
20
|
+
* it next to hotmilk (e.g. `~/.pi/npm/node_modules/context-mode`).
|
|
21
|
+
*/
|
|
22
|
+
export function resolveBundledModule(
|
|
23
|
+
relativePath: string,
|
|
24
|
+
fromModuleUrl = import.meta.url,
|
|
25
|
+
): string {
|
|
26
|
+
const { pkgName, subpath } = parseBundledModulePath(relativePath);
|
|
27
|
+
let dir = dirname(fileURLToPath(fromModuleUrl));
|
|
28
|
+
|
|
29
|
+
while (true) {
|
|
30
|
+
const nested = join(dir, "node_modules", pkgName, subpath);
|
|
31
|
+
if (existsSync(nested)) {
|
|
32
|
+
return nested;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (basename(dir) === "node_modules") {
|
|
36
|
+
const sibling = join(dir, pkgName, subpath);
|
|
37
|
+
if (existsSync(sibling)) {
|
|
38
|
+
return sibling;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(`Cannot resolve bundled module "${relativePath}" from hotmilk`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Dynamic import URL for a bundled extension entry (works with nested and hoisted installs). */
|
|
53
|
+
export function bundledImportUrl(relativePath: string): string {
|
|
54
|
+
return pathToFileURL(resolveBundledModule(relativePath)).href;
|
|
55
|
+
}
|
package/src/bootstrap/session.ts
CHANGED
|
@@ -16,6 +16,16 @@ const HOTMILK_PARSE_ERROR_MESSAGE = (path: string, error: string): string =>
|
|
|
16
16
|
const MCP_SEEDED_MESSAGE = (path: string): string =>
|
|
17
17
|
`Created ${path} from hotmilk MCP template (add servers for pi-mcp-adapter; context-mode uses the extension bridge).`;
|
|
18
18
|
|
|
19
|
+
function formatGlobalExtensionSkipsMessage(
|
|
20
|
+
skips: HotmilkRuntime["globalExtensionSkips"],
|
|
21
|
+
): string | undefined {
|
|
22
|
+
if (skips.length === 0) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const rows = skips.map((skip) => `${skip.id}: global ${skip.packageName}`).join("\n");
|
|
26
|
+
return `Bundled extensions skipped (Pi settings already provide the package):\n${rows}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function registerSessionHandlers(pi: ExtensionAPI, runtime: HotmilkRuntime): void {
|
|
20
30
|
const termProgram = process.env.TERM_PROGRAM ?? "none";
|
|
21
31
|
|
|
@@ -39,6 +49,11 @@ export function registerSessionHandlers(pi: ExtensionAPI, runtime: HotmilkRuntim
|
|
|
39
49
|
|
|
40
50
|
applyContextStackOnSessionStart(runtime, uiNotify);
|
|
41
51
|
|
|
52
|
+
const globalSkipMessage = formatGlobalExtensionSkipsMessage(runtime.globalExtensionSkips);
|
|
53
|
+
if (globalSkipMessage) {
|
|
54
|
+
uiNotify(globalSkipMessage, "info");
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
if (shouldWarnCavemanJaConflict(runtime.extensionToggles.caveman, runtime.defaults.language)) {
|
|
43
58
|
uiNotify(CAVEMAN_JA_CONFLICT_MESSAGE, "warning");
|
|
44
59
|
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** npm package name(s) that provide a bundled extension when installed via Pi settings. */
|
|
2
|
+
export type BundledPackageSpec = {
|
|
3
|
+
/** Primary npm package for `npm:<name>` entries and bundled loader resolution. */
|
|
4
|
+
packageName: string;
|
|
5
|
+
/** Extra package names that satisfy the same bundled id (e.g. dashboard bridge shim). */
|
|
6
|
+
aliases?: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type BundledExtensionLoadPhase = "context-stack" | "parallel";
|
|
10
|
+
|
|
11
|
+
export type BundledExtensionDefinition = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly package: BundledPackageSpec;
|
|
14
|
+
/** Path passed to `loadBundled()` / `bundledImportUrl()`. */
|
|
15
|
+
readonly module: string;
|
|
16
|
+
/** `/mode` section label — must appear in {@link BUNDLED_EXTENSION_GROUP_ORDER}. */
|
|
17
|
+
readonly group: string;
|
|
18
|
+
readonly loadPhase?: BundledExtensionLoadPhase;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Stable `/mode` section order (independent of manifest row order). */
|
|
22
|
+
export const BUNDLED_EXTENSION_GROUP_ORDER = [
|
|
23
|
+
"Harness",
|
|
24
|
+
"Agent tools",
|
|
25
|
+
"Context & performance",
|
|
26
|
+
"Integrations",
|
|
27
|
+
"Workflow",
|
|
28
|
+
"Output",
|
|
29
|
+
"Experiments",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
export type BundledExtensionGroupLabel = (typeof BUNDLED_EXTENSION_GROUP_ORDER)[number];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Single registry for bundled Pi extensions.
|
|
36
|
+
* Adding an extension: one row here + `hotmilk.json` default + `package.json` dep + README.
|
|
37
|
+
*/
|
|
38
|
+
export const BUNDLED_EXTENSION_DEFINITIONS = [
|
|
39
|
+
{
|
|
40
|
+
id: "skill-registry",
|
|
41
|
+
package: { packageName: "gentle-pi" },
|
|
42
|
+
module: "gentle-pi/extensions/skill-registry.ts",
|
|
43
|
+
group: "Harness",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "sdd-init",
|
|
47
|
+
package: { packageName: "gentle-pi" },
|
|
48
|
+
module: "gentle-pi/extensions/sdd-init.ts",
|
|
49
|
+
group: "Harness",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "gentle-ai",
|
|
53
|
+
package: { packageName: "gentle-pi" },
|
|
54
|
+
module: "gentle-pi/extensions/gentle-ai.ts",
|
|
55
|
+
group: "Harness",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "context-mode",
|
|
59
|
+
package: { packageName: "context-mode" },
|
|
60
|
+
module: "context-mode/build/adapters/pi/extension.js",
|
|
61
|
+
group: "Context & performance",
|
|
62
|
+
loadPhase: "context-stack",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "ask-user",
|
|
66
|
+
package: { packageName: "pi-ask-user" },
|
|
67
|
+
module: "pi-ask-user/index.ts",
|
|
68
|
+
group: "Agent tools",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "graphify",
|
|
72
|
+
package: { packageName: "graphify-pi" },
|
|
73
|
+
module: "graphify-pi/extensions/graphify.ts",
|
|
74
|
+
group: "Agent tools",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "subagents",
|
|
78
|
+
package: { packageName: "pi-subagents" },
|
|
79
|
+
module: "pi-subagents/src/extension/index.ts",
|
|
80
|
+
group: "Agent tools",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "goal",
|
|
84
|
+
package: { packageName: "pi-goal" },
|
|
85
|
+
module: "pi-goal/.pi/extensions/pi-goal/index.ts",
|
|
86
|
+
group: "Integrations",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "docparser",
|
|
90
|
+
package: { packageName: "pi-docparser" },
|
|
91
|
+
module: "pi-docparser/extensions/docparser/index.ts",
|
|
92
|
+
group: "Integrations",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "obsidian",
|
|
96
|
+
package: { packageName: "@haispeed/pi-obsidian" },
|
|
97
|
+
module: "@haispeed/pi-obsidian/extensions/obsidian-cli.ts",
|
|
98
|
+
group: "Integrations",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "btw",
|
|
102
|
+
package: { packageName: "pi-btw" },
|
|
103
|
+
module: "pi-btw/extensions/btw.ts",
|
|
104
|
+
group: "Integrations",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "simplify",
|
|
108
|
+
package: { packageName: "pi-simplify" },
|
|
109
|
+
module: "pi-simplify/dist/index.js",
|
|
110
|
+
group: "Context & performance",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "rtk-optimizer",
|
|
114
|
+
package: { packageName: "pi-rtk-optimizer" },
|
|
115
|
+
module: "pi-rtk-optimizer/index.ts",
|
|
116
|
+
group: "Context & performance",
|
|
117
|
+
loadPhase: "context-stack",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "mcp-adapter",
|
|
121
|
+
package: { packageName: "pi-mcp-adapter" },
|
|
122
|
+
module: "pi-mcp-adapter/index.ts",
|
|
123
|
+
group: "Integrations",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "planning-with-files",
|
|
127
|
+
package: { packageName: "@tomxprime/planning-with-files" },
|
|
128
|
+
module: "@tomxprime/planning-with-files/extensions/planning-with-files/index.ts",
|
|
129
|
+
group: "Workflow",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "caveman",
|
|
133
|
+
package: { packageName: "pi-caveman" },
|
|
134
|
+
module: "pi-caveman/extensions/caveman.ts",
|
|
135
|
+
group: "Output",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "red-green",
|
|
139
|
+
package: { packageName: "pi-red-green" },
|
|
140
|
+
module: "pi-red-green/dist/index.js",
|
|
141
|
+
group: "Workflow",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: "agent-dashboard",
|
|
145
|
+
package: {
|
|
146
|
+
packageName: "@blackbelt-technology/pi-agent-dashboard",
|
|
147
|
+
aliases: ["@blackbelt-technology/pi-dashboard-extension"],
|
|
148
|
+
},
|
|
149
|
+
module: "@blackbelt-technology/pi-agent-dashboard/packages/extension/src/bridge.ts",
|
|
150
|
+
group: "Agent tools",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "web-access",
|
|
154
|
+
package: { packageName: "pi-web-access" },
|
|
155
|
+
module: "pi-web-access/index.ts",
|
|
156
|
+
group: "Agent tools",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "pi-flows",
|
|
160
|
+
package: { packageName: "@blackbelt-technology/pi-flows" },
|
|
161
|
+
module: "@blackbelt-technology/pi-flows/extensions/index.ts",
|
|
162
|
+
group: "Agent tools",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "kanagawa",
|
|
166
|
+
package: { packageName: "pi-kanagawa" },
|
|
167
|
+
module: "pi-kanagawa/index.ts",
|
|
168
|
+
group: "Output",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "tetris",
|
|
172
|
+
package: { packageName: "pi-tetris" },
|
|
173
|
+
module: "pi-tetris/extensions/pi-tetris.ts",
|
|
174
|
+
group: "Experiments",
|
|
175
|
+
},
|
|
176
|
+
] as const satisfies readonly BundledExtensionDefinition[];
|
|
177
|
+
|
|
178
|
+
export type BundledExtensionId = (typeof BUNDLED_EXTENSION_DEFINITIONS)[number]["id"];
|
|
179
|
+
|
|
180
|
+
export const BUNDLED_EXTENSION_IDS: BundledExtensionId[] = BUNDLED_EXTENSION_DEFINITIONS.map(
|
|
181
|
+
(definition) => definition.id,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
export const BUNDLED_EXTENSION_PACKAGES: Record<BundledExtensionId, BundledPackageSpec> =
|
|
185
|
+
Object.fromEntries(
|
|
186
|
+
BUNDLED_EXTENSION_DEFINITIONS.map((definition) => [definition.id, definition.package]),
|
|
187
|
+
) as Record<BundledExtensionId, BundledPackageSpec>;
|
|
188
|
+
|
|
189
|
+
export const CONTEXT_STACK_EXTENSION_IDS = BUNDLED_EXTENSION_DEFINITIONS.filter(
|
|
190
|
+
(definition) => "loadPhase" in definition && definition.loadPhase === "context-stack",
|
|
191
|
+
).map((definition) => definition.id);
|
|
192
|
+
|
|
193
|
+
export type BundledExtensionGroup = {
|
|
194
|
+
label: BundledExtensionGroupLabel;
|
|
195
|
+
ids: BundledExtensionId[];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export function buildBundledExtensionGroups(): BundledExtensionGroup[] {
|
|
199
|
+
const idsByGroup = new Map<BundledExtensionGroupLabel, BundledExtensionId[]>();
|
|
200
|
+
for (const label of BUNDLED_EXTENSION_GROUP_ORDER) {
|
|
201
|
+
idsByGroup.set(label, []);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const definition of BUNDLED_EXTENSION_DEFINITIONS) {
|
|
205
|
+
const group = definition.group as BundledExtensionGroupLabel;
|
|
206
|
+
if (!idsByGroup.has(group)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Bundled extension ${definition.id} uses unknown group "${definition.group}" — add it to BUNDLED_EXTENSION_GROUP_ORDER`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
idsByGroup.get(group)!.push(definition.id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return BUNDLED_EXTENSION_GROUP_ORDER.map((label) => ({
|
|
215
|
+
label,
|
|
216
|
+
ids: idsByGroup.get(label)!,
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const BUNDLED_EXTENSION_GROUPS = buildBundledExtensionGroups();
|
|
221
|
+
|
|
222
|
+
const groupedIds = new Set(BUNDLED_EXTENSION_GROUPS.flatMap((group) => group.ids));
|
|
223
|
+
for (const id of BUNDLED_EXTENSION_IDS) {
|
|
224
|
+
if (!groupedIds.has(id)) {
|
|
225
|
+
throw new Error(`BUNDLED_EXTENSION_GROUPS is missing extension id: ${id}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const seenIds = new Set<string>();
|
|
230
|
+
for (const definition of BUNDLED_EXTENSION_DEFINITIONS) {
|
|
231
|
+
if (seenIds.has(definition.id)) {
|
|
232
|
+
throw new Error(`Duplicate bundled extension id: ${definition.id}`);
|
|
233
|
+
}
|
|
234
|
+
seenIds.add(definition.id);
|
|
235
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUNDLED_EXTENSION_PACKAGES,
|
|
3
|
+
type BundledExtensionId,
|
|
4
|
+
type BundledPackageSpec,
|
|
5
|
+
} from "./bundled-extensions.ts";
|
|
6
|
+
|
|
7
|
+
export type { BundledPackageSpec };
|
|
8
|
+
|
|
9
|
+
export { BUNDLED_EXTENSION_PACKAGES };
|
|
10
|
+
|
|
11
|
+
export const HOTMILK_PACKAGE_NAME = "hotmilk";
|
|
12
|
+
|
|
13
|
+
export function packageNamesForBundledExtension(id: BundledExtensionId): readonly string[] {
|
|
14
|
+
const spec = BUNDLED_EXTENSION_PACKAGES[id];
|
|
15
|
+
return spec.aliases ? [spec.packageName, ...spec.aliases] : [spec.packageName];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function detectGlobalProviderForBundledExtension(
|
|
19
|
+
id: BundledExtensionId,
|
|
20
|
+
installedPackageNames: ReadonlySet<string>,
|
|
21
|
+
): string | undefined {
|
|
22
|
+
for (const name of packageNamesForBundledExtension(id)) {
|
|
23
|
+
if (installedPackageNames.has(name)) {
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
package/src/config/hotmilk.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { resolveBundledExtensionToggles } from "./resolve.ts";
|
|
6
|
+
import { BUNDLED_EXTENSION_IDS, type BundledExtensionId } from "./bundled-extensions.ts";
|
|
6
7
|
|
|
7
8
|
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
9
|
const BUNDLED_TEMPLATE_PATH = join(PACKAGE_ROOT, "hotmilk.json");
|
|
@@ -52,31 +53,7 @@ function buildDefaultConfigFromTemplate(template: HotmilkConfig): {
|
|
|
52
53
|
export const CONFIG_FILENAME = "hotmilk.json";
|
|
53
54
|
export const AGENT_HOTMILK_CONFIG_LABEL = "~/.pi/agent/hotmilk.json";
|
|
54
55
|
|
|
55
|
-
export
|
|
56
|
-
"skill-registry",
|
|
57
|
-
"sdd-init",
|
|
58
|
-
"gentle-ai",
|
|
59
|
-
"context-mode",
|
|
60
|
-
"ask-user",
|
|
61
|
-
"graphify",
|
|
62
|
-
"subagents",
|
|
63
|
-
"goal",
|
|
64
|
-
"docparser",
|
|
65
|
-
"obsidian",
|
|
66
|
-
"cursor-provider",
|
|
67
|
-
"btw",
|
|
68
|
-
"simplify",
|
|
69
|
-
"rtk-optimizer",
|
|
70
|
-
"mcp-adapter",
|
|
71
|
-
"planning-with-files",
|
|
72
|
-
"caveman",
|
|
73
|
-
"red-green",
|
|
74
|
-
"agent-dashboard",
|
|
75
|
-
"web-access",
|
|
76
|
-
"pi-flows",
|
|
77
|
-
] as const;
|
|
78
|
-
|
|
79
|
-
export type BundledExtensionId = (typeof BUNDLED_EXTENSION_IDS)[number];
|
|
56
|
+
export { BUNDLED_EXTENSION_IDS, type BundledExtensionId } from "./bundled-extensions.ts";
|
|
80
57
|
|
|
81
58
|
export type PersonaMode = "gentleman" | "neutral";
|
|
82
59
|
|
package/src/config/runtime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GlobalBundledExtensionSkip } from "../bootstrap/global-extension-sources.ts";
|
|
1
2
|
import {
|
|
2
3
|
loadHotmilkConfig,
|
|
3
4
|
type BundledExtensionId,
|
|
@@ -18,6 +19,8 @@ export type HotmilkRuntime = {
|
|
|
18
19
|
configPath: string;
|
|
19
20
|
configError?: string;
|
|
20
21
|
extensionToggles: Record<BundledExtensionId, boolean>;
|
|
22
|
+
/** Bundled ids skipped because the same npm package is in Pi settings. */
|
|
23
|
+
globalExtensionSkips: GlobalBundledExtensionSkip[];
|
|
21
24
|
defaults: ResolvedDefaults;
|
|
22
25
|
graph: ResolvedGraphSettings;
|
|
23
26
|
mcp: ResolvedMcpSettings;
|
|
@@ -30,6 +33,7 @@ export function createHotmilkRuntime(configRoot?: string): HotmilkRuntime {
|
|
|
30
33
|
configPath: loaded.path,
|
|
31
34
|
configError: loaded.error,
|
|
32
35
|
extensionToggles: resolveBundledExtensionToggles(loaded.config),
|
|
36
|
+
globalExtensionSkips: [],
|
|
33
37
|
defaults: resolveDefaults(loaded.config),
|
|
34
38
|
graph: resolveGraphSettings(loaded.config),
|
|
35
39
|
mcp: resolveMcpSettings(loaded.config),
|
package/src/controller/input.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { ExtensionContext } from "./context.ts";
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { openModeSettingsModal } from "./mode.ts";
|
|
4
3
|
|
|
5
4
|
const INTERRUPT_PREFIX = "/interrupt ";
|
package/src/controller/mode.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { getSettingsListTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Container, SettingsList, Text, type SettingItem } from "@earendil-works/pi-tui";
|
|
3
|
-
import { BUNDLED_EXTENSION_GROUPS } from "../config/
|
|
3
|
+
import { BUNDLED_EXTENSION_GROUPS } from "../config/bundled-extensions.ts";
|
|
4
4
|
import {
|
|
5
5
|
AGENT_HOTMILK_CONFIG_LABEL,
|
|
6
6
|
type BundledExtensionId,
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
loadHotmilkConfig,
|
|
9
9
|
saveHotmilkConfig,
|
|
10
10
|
} from "../config/hotmilk.ts";
|
|
11
|
-
import type { ExtensionContext } from "./context.ts";
|
|
12
11
|
|
|
13
12
|
function formatToggleState(enabled: boolean): "on" | "off" {
|
|
14
13
|
return enabled ? "on" : "off";
|
package/src/index.ts
CHANGED
|
@@ -15,18 +15,18 @@ import { registerHotmilkSessionLogo } from "./ui/session-logo.ts";
|
|
|
15
15
|
export default async function registerHotmilk(pi: ExtensionAPI): Promise<void> {
|
|
16
16
|
const runtime = createHotmilkRuntime();
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
scheduleDashboardWarmStart();
|
|
21
|
-
}
|
|
18
|
+
// Register before bundled imports so session_start handlers exist when bindExtensions emits.
|
|
19
|
+
registerHotmilkSessionLogo(pi);
|
|
22
20
|
|
|
23
21
|
prepareContextStack(runtime.extensionToggles);
|
|
24
22
|
|
|
25
|
-
await registerBundledExtensions(pi, runtime.extensionToggles
|
|
23
|
+
const bundled = await registerBundledExtensions(pi, runtime.extensionToggles, {
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
});
|
|
26
|
+
runtime.globalExtensionSkips = bundled.globalSkips;
|
|
26
27
|
registerGraphHandlers(pi, runtime.graph);
|
|
27
28
|
registerDefaultsHandlers(pi, runtime.defaults);
|
|
28
29
|
registerSessionHandlers(pi, runtime);
|
|
29
30
|
registerInputCommands(pi);
|
|
30
|
-
registerHotmilkSessionLogo(pi);
|
|
31
31
|
pi.on("input", (event, ctx) => routeInputCommand(event.text, pi, ctx));
|
|
32
32
|
}
|