@pi-harness/pi-harness 0.1.29 → 0.1.31

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 (53) hide show
  1. package/README.md +1 -1
  2. package/apps/web/dist/assets/index-D9ZcvGXi.js +25 -0
  3. package/apps/web/dist/assets/{index-xF647pQJ.css → index-DJsNIiIo.css} +1 -1
  4. package/apps/web/dist/index.html +2 -2
  5. package/apps/web/package.json +1 -15
  6. package/apps/web/profile/cordis.yml +0 -221
  7. package/apps/web/server-dist/bin.js +4 -2
  8. package/apps/web/server-dist/bin.js.map +1 -1
  9. package/apps/web/src/server/bin.ts +4 -2
  10. package/apps/web/src/style.css +3 -0
  11. package/apps/web/test/bin.test.ts +6 -0
  12. package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
  13. package/node_modules/@pi-harness/api-gateway/dist/index.js +43 -12
  14. package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
  15. package/node_modules/@pi-harness/api-gateway/package.json +1 -27
  16. package/node_modules/@pi-harness/cli/dist/bin.js +2 -1
  17. package/node_modules/@pi-harness/cli/dist/bin.js.map +1 -1
  18. package/node_modules/@pi-harness/cli/dist/main.d.ts +2 -0
  19. package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
  20. package/node_modules/@pi-harness/cli/dist/main.js +8 -1
  21. package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
  22. package/node_modules/@pi-harness/cli/package.json +1 -1
  23. package/node_modules/@pi-harness/cli/profiles/default/cordis.yml +0 -87
  24. package/node_modules/@pi-harness/host-webserver/package.json +1 -1
  25. package/node_modules/@pi-harness/web-app/package.json +1 -1
  26. package/package.json +8 -35
  27. package/packages/api-gateway/dist/index.d.ts.map +1 -1
  28. package/packages/api-gateway/dist/index.js +43 -12
  29. package/packages/api-gateway/dist/index.js.map +1 -1
  30. package/packages/api-gateway/package.json +1 -27
  31. package/packages/api-gateway/src/index.ts +55 -17
  32. package/packages/api-gateway/test/index.test.ts +157 -2
  33. package/packages/api-gateway/test/packed-package.test.ts +106 -10
  34. package/packages/api-gateway/test/release-contract.test.ts +10 -5
  35. package/packages/bundle-web-app/package.json +1 -1
  36. package/packages/cli/dist/bin.js +2 -1
  37. package/packages/cli/dist/bin.js.map +1 -1
  38. package/packages/cli/dist/main.d.ts +2 -0
  39. package/packages/cli/dist/main.d.ts.map +1 -1
  40. package/packages/cli/dist/main.js +8 -1
  41. package/packages/cli/dist/main.js.map +1 -1
  42. package/packages/cli/package.json +1 -1
  43. package/packages/cli/profiles/default/cordis.yml +0 -87
  44. package/packages/cli/src/bin.ts +2 -1
  45. package/packages/cli/src/main.ts +10 -1
  46. package/packages/cli/test/binary.test.ts +5 -1
  47. package/packages/cli/test/broken-pipe.test.ts +1 -1
  48. package/packages/cli/test/cli.test.ts +1 -0
  49. package/packages/cli/test/main.test.ts +1 -0
  50. package/packages/cli/test/profiles/plugins/cordis.yml +122 -0
  51. package/packages/cli/test/profiles.integration.test.ts +40 -23
  52. package/packages/host-webserver/package.json +1 -1
  53. package/apps/web/dist/assets/index-DCkzrlCA.js +0 -25
@@ -26,97 +26,10 @@
26
26
  - bash
27
27
  - edit
28
28
  - write
29
- - id: agent-teams
30
- name: "@pi-harness/plugin-agent-teams"
31
- config: {}
32
- - id: modlens
33
- name: "@pi-harness/plugin-modlens"
34
- config: {}
35
- - id: vision-toolkit
36
- name: "@pi-harness/plugin-vision-toolkit"
37
- config: {}
38
- - id: git-time-capsule
39
- name: "@pi-harness/plugin-git-time-capsule"
40
- config: {}
41
- - id: dependency-checker
42
- name: "@pi-harness/plugin-dependency-checker"
43
- config: {}
44
- - id: at-file
45
- name: "@pi-harness/plugin-at-file"
46
- config: {}
47
- - id: test-harness
48
- name: "@pi-harness/plugin-test-harness"
49
- config:
50
- timeoutMs: 120000
51
- - id: session-insights
52
- name: "@pi-harness/plugin-session-insights"
53
- config: {}
54
- - id: readme-gen
55
- name: "@pi-harness/plugin-readme-gen"
56
- config: {}
57
- - id: i18n-pair
58
- name: "@pi-harness/plugin-i18n-pair"
59
- config: {}
60
- - id: cleaner
61
- name: "@pi-harness/plugin-cleaner"
62
- config: {}
63
- - id: sql-lens
64
- name: "@pi-harness/plugin-sql-lens"
65
- config:
66
- timeoutMs: 5000
67
- - id: docker-sandbox
68
- name: "@pi-harness/plugin-docker-sandbox"
69
- config: {}
70
- - id: mcp-client
71
- name: "@pi-harness/plugin-mcp-client"
72
- config: {}
73
- - id: browser-fetch
74
- name: "@pi-harness/plugin-browser-fetch"
75
- config: {}
76
- - id: browser-session
77
- name: "@pi-harness/plugin-browser-session"
78
- config: {}
79
- - id: plugin-stars
80
- name: "@pi-harness/plugin-plugin-stars"
81
- config: {}
82
- - id: yaml-validator
83
- name: "@pi-harness/plugin-yaml-validator"
84
- config: {}
85
- - id: plugin-dev
86
- name: "@pi-harness/plugin-plugin-dev"
87
- config: {}
88
- - id: openpets
89
- name: "@pi-harness/plugin-openpets"
90
- config: {}
91
- - id: session-bridge
92
- name: "@pi-harness/plugin-session-bridge"
93
- config: {}
94
- - id: skill-guard
95
- name: "@pi-harness/plugin-skill-guard"
96
- config: {}
97
- - id: recall-unread
98
- name: "@pi-harness/plugin-recall-unread"
99
- config: {}
100
- - id: turn-rewind
101
- name: "@pi-harness/plugin-turn-rewind"
102
- config: {}
103
- - id: context-doctor
104
- name: "@pi-harness/plugin-context-doctor"
105
- config: {}
106
29
  - id: runtime
107
30
  name: "@pi-harness/core/plugins/runtime"
108
31
  config:
109
32
  thinkingLevel: medium
110
- - id: context
111
- name: "@pi-harness/plugin-context"
112
- config: {}
113
- - id: token-guard
114
- name: "@pi-harness/plugin-token-guard"
115
- config:
116
- maxPercent: 90
117
- - id: fail-logger
118
- name: "@pi-harness/plugin-fail-logger"
119
- config: {}
120
33
  - id: stdio
121
34
  name: "@pi-harness/core/plugins/stdio"
122
35
  config: {}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/host-webserver",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "private": true,
5
5
  "description": "Cordis route-registration service for the Pi Harness web host",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/web-app",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "private": true,
5
5
  "description": "Patchable Web application bundle for Pi Harness",
6
6
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/pi-harness",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "homepage": "https://github.com/pi-harness/pi-harness#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/pi-harness/pi-harness/issues"
@@ -34,7 +34,8 @@
34
34
  "web": "npm run build:web && node apps/web/server-dist/bin.js",
35
35
  "format": "vp fmt . --write",
36
36
  "format:check": "vp fmt . --check",
37
- "prepack": "npm run build:web",
37
+ "prepack": "npm run build:web && node scripts/prepare-published-manifest.mjs --strip",
38
+ "postpack": "node scripts/prepare-published-manifest.mjs --restore",
38
39
  "test:package": "node scripts/test-packed-package.mjs",
39
40
  "clean": "npm run clean --workspaces --if-present && node -e \"for (const target of ['packages/api-gateway/dist','packages/bundle-web-app/dist','packages/client-web/dist','packages/host-webserver/dist','apps/web/dist','apps/web/server-dist']) require('node:fs').rmSync(target,{recursive:true,force:true})\"",
40
41
  "lint": "npm run build && npm run lint:check",
@@ -56,39 +57,11 @@
56
57
  "@earendil-works/pi-agent-core": "0.84.4",
57
58
  "@earendil-works/pi-ai": "0.84.4",
58
59
  "@earendil-works/pi-coding-agent": "0.84.4",
59
- "@pi-harness/api-gateway": "0.1.29",
60
- "@pi-harness/cli": "0.1.29",
61
- "@pi-harness/core": "^0.1.29",
62
- "@pi-harness/host-webserver": "0.1.29",
63
- "@pi-harness/plugin-agent-teams": "0.1.29",
64
- "@pi-harness/plugin-at-file": "0.1.29",
65
- "@pi-harness/plugin-browser-fetch": "0.1.29",
66
- "@pi-harness/plugin-browser-session": "0.1.29",
67
- "@pi-harness/plugin-cleaner": "0.1.29",
68
- "@pi-harness/plugin-context": "0.1.29",
69
- "@pi-harness/plugin-context-doctor": "0.1.29",
70
- "@pi-harness/plugin-dependency-checker": "0.1.29",
71
- "@pi-harness/plugin-docker-sandbox": "0.1.29",
72
- "@pi-harness/plugin-fail-logger": "0.1.29",
73
- "@pi-harness/plugin-git-time-capsule": "0.1.29",
74
- "@pi-harness/plugin-i18n-pair": "0.1.29",
75
- "@pi-harness/plugin-mcp-client": "0.1.29",
76
- "@pi-harness/plugin-modlens": "0.1.29",
77
- "@pi-harness/plugin-openpets": "0.1.29",
78
- "@pi-harness/plugin-plugin-dev": "0.1.29",
79
- "@pi-harness/plugin-plugin-stars": "0.1.29",
80
- "@pi-harness/plugin-readme-gen": "0.1.29",
81
- "@pi-harness/plugin-recall-unread": "0.1.29",
82
- "@pi-harness/plugin-session-bridge": "0.1.29",
83
- "@pi-harness/plugin-session-insights": "0.1.29",
84
- "@pi-harness/plugin-skill-guard": "0.1.29",
85
- "@pi-harness/plugin-sql-lens": "0.1.29",
86
- "@pi-harness/plugin-test-harness": "0.1.29",
87
- "@pi-harness/plugin-token-guard": "0.1.29",
88
- "@pi-harness/plugin-turn-rewind": "0.1.29",
89
- "@pi-harness/plugin-vision-toolkit": "0.1.29",
90
- "@pi-harness/plugin-yaml-validator": "0.1.29",
91
- "@pi-harness/web-app": "0.1.29",
60
+ "@pi-harness/api-gateway": "0.1.31",
61
+ "@pi-harness/cli": "0.1.31",
62
+ "@pi-harness/core": "^0.1.31",
63
+ "@pi-harness/host-webserver": "0.1.31",
64
+ "@pi-harness/web-app": "0.1.31",
92
65
  "semver": "7.8.5"
93
66
  },
94
67
  "devDependencies": {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;;;;mBA4sBlC,OAAO;;AAHxB,wBAg4CE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;;;;mBAmuBlC,OAAO;;AAHxB,wBA+4CE"}
@@ -3,10 +3,11 @@ import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, unlink, writeFile
3
3
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { SessionManager } from "@earendil-works/pi-coding-agent";
6
- import { atomicWriteFile } from "@pi-harness/core";
6
+ import { atomicWriteFile, isPiToolRegistryLeasedError } from "@pi-harness/core";
7
7
  import { MARKETPLACE_CAPABILITIES, MARKETPLACE_CATEGORIES, MARKETPLACE_PLUGINS, attachMarketplaceStatistics, createMarketplaceStatisticsLoader, paginateMarketplace, searchMarketplace, marketplaceNpmPackageName, sortMarketplaceByRecommendation, } from "./marketplace.js";
8
8
  import { parseGitWorktrees } from "./workspaces.js";
9
9
  const loadMarketplaceStatistics = createMarketplaceStatisticsLoader();
10
+ const RUNTIME_ENTRY_NAME = "@pi-harness/core/plugins/runtime";
10
11
  const DEFAULT_BODY_LIMIT_BYTES = 64 * 1024;
11
12
  const IMPORT_CONTENT_LIMIT_BYTES = 10 * 1024 * 1024;
12
13
  // The import body carries the JSONL content as a JSON string, so quotes and newlines are escaped; leave headroom above the content limit for that overhead.
@@ -434,15 +435,23 @@ function runProcess(executable, args, cwd) {
434
435
  });
435
436
  });
436
437
  }
437
- async function appendMarketplaceProfile(configPath, plugin) {
438
+ // A plugin registers its tools before pi-runtime acquires the tool registry, so an installed entry has to sit in the runtime's own group ahead of the runtime. Appended at the end of the profile it would load after the runtime and fail on every subsequent start.
439
+ function marketplaceProfileEntry(plugin, indent) {
440
+ const child = `${indent} `;
441
+ const group = plugin.profile.group === true ? `\n${child}group: true` : "";
442
+ return `${indent}- id: marketplace-${plugin.id}\n${child}name: ${JSON.stringify(plugin.packageName)}${group}\n${child}config: ${JSON.stringify(plugin.profile.config)}\n`;
443
+ }
444
+ async function appendMarketplaceProfile(configPath, plugin, beforeEntryId) {
438
445
  const source = await readFile(configPath, "utf8");
439
446
  if (source.includes(`name: ${JSON.stringify(plugin.packageName)}`))
440
447
  return source;
441
- const entryId = `marketplace-${plugin.id}`;
442
- const config = JSON.stringify(plugin.profile.config);
443
- const group = plugin.profile.group === true ? "\n group: true" : "";
444
- const entry = `\n- id: ${entryId}\n name: ${JSON.stringify(plugin.packageName)}${group}\n config: ${config}\n`;
445
- await writeFile(configPath, source.replace(/\s*$/, "") + entry, "utf8");
448
+ const anchor = beforeEntryId === undefined ? null : source.match(new RegExp(`^([ \\t]*)- id: ${beforeEntryId.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}[ \\t]*$`, "mu"));
449
+ if (anchor === null) {
450
+ await writeFile(configPath, `${source.replace(/\s*$/u, "")}\n${marketplaceProfileEntry(plugin, "")}`, "utf8");
451
+ return source;
452
+ }
453
+ const start = anchor.index ?? 0;
454
+ await writeFile(configPath, source.slice(0, start) + marketplaceProfileEntry(plugin, anchor[1] ?? "") + source.slice(start), "utf8");
446
455
  return source;
447
456
  }
448
457
  async function updateMarketplaceProfile(configPath, entryId, update) {
@@ -590,7 +599,7 @@ async function isFile(path) {
590
599
  return false;
591
600
  }
592
601
  }
593
- // The loader resolves a bare plugin specifier from the profile file upwards (`createRequire(profilePath).resolve(name)`), never from the shell the harness was started in, so a marketplace package has to land in the package that owns the profile or it installs successfully and then fails to load.
602
+ // The loader resolves a bare plugin specifier by walking node_modules from the profile file upwards, never from the shell the harness was started in, so a marketplace package has to land in the package that owns the profile or it installs successfully and then fails to load. Under a built-in profile that package is the harness home, which is why the launcher boots a copy there instead of the read-only one inside its own installation.
594
603
  async function marketplaceInstallDirectory(configPath, fallback) {
595
604
  let directory = dirname(resolve(configPath));
596
605
  let parent = dirname(directory);
@@ -602,6 +611,17 @@ async function marketplaceInstallDirectory(configPath, fallback) {
602
611
  }
603
612
  return fallback;
604
613
  }
614
+ // Where an installed plugin belongs in the running loader: inside the group that owns pi-runtime, immediately ahead of it. A profile without a runtime entry (the stdio CLI profile does have one, a hand-written profile may not) places the entry at the end of the root instead.
615
+ function runtimePlacement(loader) {
616
+ const entries = [...loader.entries()];
617
+ const runtime = entries.find((entry) => entry.options.name === RUNTIME_ENTRY_NAME);
618
+ if (runtime === undefined)
619
+ return undefined;
620
+ const group = entries.find((entry) => entry.subgroup === runtime.parent);
621
+ const position = runtime.parent.data.indexOf(runtime.options);
622
+ // The loader addresses a nested group by its qualified id (`profile:agent`), while the profile file names the entry by its own id.
623
+ return { groupEntryId: group?.id ?? null, position: position < 0 ? Infinity : position, beforeEntryId: runtime.options.id };
624
+ }
605
625
  function pickDirectory() {
606
626
  return new Promise((resolvePath, reject) => {
607
627
  execFile("osascript", ["-e", 'POSIX path of (choose folder with prompt "选择 Pi 工作区")'], { timeout: 120_000, maxBuffer: 64 * 1024 }, (error, stdout, stderr) => {
@@ -1057,27 +1077,33 @@ export default {
1057
1077
  const packageLockPath = join(installDirectory, "package-lock.json");
1058
1078
  const packageJsonBefore = await readFile(packageJsonPath, "utf8").catch(() => undefined);
1059
1079
  const packageLockBefore = await readFile(packageLockPath, "utf8").catch(() => undefined);
1080
+ const placement = runtimePlacement(loader);
1060
1081
  let profileBefore;
1061
1082
  let entryId;
1062
1083
  try {
1063
1084
  const specifier = `${marketplaceNpmPackageName(plugin.packageName)}@${plugin.version}`;
1064
1085
  await runProcess("npm", ["install", "--save-exact", "--package-lock=false", specifier], installDirectory);
1065
- profileBefore = await appendMarketplaceProfile(configPath, plugin);
1086
+ profileBefore = await appendMarketplaceProfile(configPath, plugin, placement?.beforeEntryId);
1066
1087
  entryId = await loader.create({
1067
1088
  id: `marketplace-${plugin.id}`,
1068
1089
  name: plugin.packageName,
1069
1090
  ...(plugin.profile.group === true ? { group: true } : {}),
1070
1091
  config: plugin.profile.config,
1071
- });
1092
+ }, placement?.groupEntryId ?? null, placement?.position ?? Infinity);
1072
1093
  const entry = loader.resolve(entryId);
1073
1094
  if (entry.fiber === undefined)
1074
1095
  throw new Error(`Plugin ${plugin.packageName} did not create a runtime fiber`);
1075
1096
  await entry.fiber.await();
1076
- sendJson(response, 200, { plugin, installed: true });
1097
+ sendJson(response, 200, { plugin, installed: true, restartRequired: false });
1077
1098
  }
1078
1099
  catch (error) {
1079
1100
  if (entryId !== undefined)
1080
1101
  await loader.remove(entryId).catch(() => { });
1102
+ // The runtime holds the tool registry for its whole life and snapshots the tool set when it takes it, so a plugin that contributes tools cannot join a harness that is already running. The package and its profile entry stay in place and the plugin arrives on the next start; rolling the install back would leave the user unable to install it at all.
1103
+ if (isPiToolRegistryLeasedError(error)) {
1104
+ sendJson(response, 200, { plugin, installed: true, restartRequired: true });
1105
+ return;
1106
+ }
1081
1107
  if (profileBefore !== undefined)
1082
1108
  await writeFile(configPath, profileBefore, "utf8").catch(() => { });
1083
1109
  // A manifest npm created for this install is removed rather than left behind; restoring is only possible when one existed before.
@@ -1138,9 +1164,14 @@ export default {
1138
1164
  const before = await updateMarketplaceProfile(configPath, profileEntryId, { disabled: !payload.enabled });
1139
1165
  try {
1140
1166
  await entry.update({ disabled: !payload.enabled });
1141
- sendJson(response, 200, { plugin: pluginSummary(entry) });
1167
+ sendJson(response, 200, { plugin: pluginSummary(entry), restartRequired: false });
1142
1168
  }
1143
1169
  catch (error) {
1170
+ // Re-enabling a plugin that contributes tools cannot take effect in a harness the runtime already leased, so the profile keeps the change and the plugin comes back on the next start. Reverting it would leave a plugin that can be switched off but never on again.
1171
+ if (isPiToolRegistryLeasedError(error)) {
1172
+ sendJson(response, 200, { plugin: pluginSummary(entry), restartRequired: true });
1173
+ return;
1174
+ }
1144
1175
  await writeFile(configPath, before, "utf8").catch(() => { });
1145
1176
  sendJson(response, 502, { error: errorText(error) });
1146
1177
  }