@computesdk/loops 0.1.12 → 0.1.13

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
@@ -195,6 +195,11 @@ The default runner uses Pi 1.4.0's own CLI with a narrow, in-memory compatibilit
195
195
  before awaiting startup extensions, handle their answers immediately, and hold ordinary commands until setup finishes.
196
196
  It verifies the pinned version and exact upstream source before applying the fix; no global Pi files are modified.
197
197
  Pi still owns the session, extensions, model configuration, and all decisions.
198
+
199
+ The bundled Loops extension is a fallback: if Pi's enabled, trusted packages already supply
200
+ `@computesdk/pi-loops`, the default runner uses that installation instead of loading a second copy.
201
+ Other extensions remain enabled, and genuine conflicts still fail. This selection is also SHA-pinned
202
+ to Pi 1.4.0's resource loader; explicit `--extensions` or `--pi` retain normal Pi loading semantics.
198
203
  Failed answer submissions remain visible with a retryable error; Loops never accepts or declines an integration for you.
199
204
  An explicit `--pi` bypasses this compatibility runner, so that executable must support startup dialogs itself.
200
205
 
@@ -0,0 +1,47 @@
1
+ // The UI supplies Loops only as a fallback. Let Pi's own enabled-resource and
2
+ // trust resolution choose a workspace/user installation before loading code.
3
+ import { createHash } from 'node:crypto';
4
+ import { readFileSync, realpathSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const expected = 'ad50235884ea6d5ee14cdae1efcfe6afef58b22b18ce574e81886d57ca9f4642';
8
+ const canonical = (path) => {
9
+ try { return realpathSync(path); } catch { return path; }
10
+ };
11
+
12
+ export function filterDefaultLoopsExtension(paths, metadataByPath, fallback, bootstrap = false) {
13
+ const fallbackPath = canonical(fallback);
14
+ // The app's fallback is not a trust provider. Defer it until Pi has made its
15
+ // trust decision, otherwise its factory can run before a project copy wins.
16
+ if (bootstrap) return paths.filter((path) => canonical(path) !== fallbackPath);
17
+ const hasInstalledLoops = paths.some((path) => {
18
+ if (canonical(path) === fallbackPath) return false;
19
+ // Do not infer identity from a filename, tool name, or package source string.
20
+ // Only roots already discovered by Pi as a package are eligible.
21
+ const root = metadataByPath.get(path)?.packageRoot;
22
+ if (!root) return false;
23
+ try {
24
+ return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).name === '@computesdk/pi-loops';
25
+ } catch { return false; }
26
+ });
27
+ return hasInstalledLoops ? paths.filter((path) => canonical(path) !== fallbackPath) : paths;
28
+ }
29
+
30
+ export function patchDefaultLoopsExtension(source, fallback) {
31
+ if (createHash('sha256').update(source).digest('hex') !== expected) {
32
+ throw new Error('Unsupported Pi resource loader source; default Loops compatibility patch requires Pi 1.4.0');
33
+ }
34
+ // extensionsOverride runs after extension execution and conflict detection.
35
+ // Filter only our implicit CLI fallback, before either operation, in both the
36
+ // untrusted bootstrap pass and the final trusted/untrusted resource pass.
37
+ const declaration = 'const extensionPaths = (this.noExtensions ? cliEnabledExtensions : this.mergePaths(cliEnabledExtensions, enabledExtensions))';
38
+ const warnings = ' const packageWarnings = collectExtensionPackageWarnings(extensionPaths, metadataByPath);';
39
+ const bootstrapMethod = ' async loadCurrentExtensionSet(options) {';
40
+ if (source.split(declaration).length !== 3 || source.split(warnings).length !== 3 || source.split(bootstrapMethod).length !== 2) {
41
+ throw new Error('Pi default Loops patch anchor mismatch');
42
+ }
43
+ source = source.replaceAll(declaration, declaration.replace('extensionPaths', 'discoveredExtensionPaths'));
44
+ source = source.split(bootstrapMethod).map((part, index) => part.replace(warnings,
45
+ ` const extensionPaths = filterDefaultLoopsExtension(discoveredExtensionPaths, metadataByPath, ${JSON.stringify(fallback)}, ${index === 1});\n${warnings}`)).join(bootstrapMethod);
46
+ return `import { filterDefaultLoopsExtension } from ${JSON.stringify(import.meta.url)};\n${source}`;
47
+ }
package/bin/pi-rpc.mjs CHANGED
@@ -5,6 +5,7 @@ import { readFileSync, realpathSync } from 'node:fs';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { patchRpcStartup } from './pi-rpc-startup.mjs';
8
+ import { patchDefaultLoopsExtension } from './pi-default-extension.mjs';
8
9
 
9
10
  const main = fileURLToPath(import.meta.resolve('@earendil-works/pi-coding-agent'));
10
11
  const root = dirname(dirname(main));
@@ -13,14 +14,27 @@ if (pkg.name !== '@earendil-works/pi-coding-agent' || pkg.version !== '1.4.0') {
13
14
  throw new Error('Pi RPC runner requires @earendil-works/pi-coding-agent 1.4.0');
14
15
  }
15
16
  const target = realpathSync(join(root, 'dist/modes/rpc/rpc-mode.js'));
17
+ // This private runner flag is supplied only for the app's implicit default.
18
+ // Explicit --extensions and custom Pi executables retain normal CLI semantics.
19
+ const defaultIndex = process.argv.indexOf('--loops-default-extension');
20
+ const fallback = defaultIndex < 0 ? undefined : process.argv[defaultIndex + 1];
21
+ if (defaultIndex >= 0) {
22
+ if (!fallback || fallback.startsWith('-')) throw new Error('--loops-default-extension requires a path');
23
+ process.argv.splice(defaultIndex, 2, '-e', fallback);
24
+ }
25
+ const resourceTarget = fallback ? realpathSync(join(root, 'dist/core/resource-loader.js')) : undefined;
16
26
  // Validate before the CLI starts, including when a future CLI stops loading RPC.
17
27
  patchRpcStartup(readFileSync(target, 'utf8'));
28
+ if (resourceTarget) patchDefaultLoopsExtension(readFileSync(resourceTarget, 'utf8'), fallback);
18
29
  registerHooks({
19
30
  load(url, context, nextLoad) {
20
31
  const result = nextLoad(url, context);
21
32
  if (url.startsWith('file:') && fileURLToPath(url) === target) {
22
33
  return { ...result, source: patchRpcStartup(String(result.source)) };
23
34
  }
35
+ if (resourceTarget && url.startsWith('file:') && fileURLToPath(url) === resourceTarget) {
36
+ return { ...result, source: patchDefaultLoopsExtension(String(result.source), fallback) };
37
+ }
24
38
  return result;
25
39
  },
26
40
  });
package/dist/server.mjs CHANGED
@@ -5308,6 +5308,7 @@ var PI = arg("pi", path10.join(APP_ROOT, "bin", "pi-rpc.mjs"));
5308
5308
  var DATA = path10.resolve(arg("data", process.env.LOOPS_UI_DATA ?? path10.join(process.env.HOME ?? process.cwd(), ".pi", "loops-ui")));
5309
5309
  var MODEL = arg("model", void 0) || null;
5310
5310
  var EXTENSIONS = (arg("extensions", path10.resolve(APP_ROOT, "../pi-loops/extensions/loop.ts")) ?? "").split(",").filter(Boolean);
5311
+ var IMPLICIT_BUNDLED_EXTENSION = arg("pi") === void 0 && arg("extensions") === void 0;
5311
5312
  function prerequisites() {
5312
5313
  const out = [];
5313
5314
  const onPath = PI.includes("/") || /^[A-Za-z]:\\/.test(PI) ? fs9.existsSync(PI) : (() => {
@@ -5374,7 +5375,7 @@ var requireChatWorkspace = (id) => {
5374
5375
  };
5375
5376
  var manager = new ChatManager({
5376
5377
  piCommand: PI,
5377
- args: EXTENSIONS.flatMap((e) => ["-e", e]),
5378
+ args: EXTENSIONS.flatMap((e) => [IMPLICIT_BUNDLED_EXTENSION ? "--loops-default-extension" : "-e", e]),
5378
5379
  model: MODEL,
5379
5380
  // a chat's repo comes from the registry: the browser never hands us a path
5380
5381
  bind: (chat) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@computesdk/loops",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Loops for people who do not live in a terminal: one command starts a localhost UI that runs, watches and steers hill-climbing agents in your repos.",
6
6
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  "start": "node --experimental-strip-types server/index.ts",
38
38
  "check": "tsc --noEmit && tsc --noEmit -p tsconfig.client.json",
39
39
  "shots": "node shots.mjs",
40
- "test": "node --experimental-strip-types test/ui-compaction.ts && node --experimental-strip-types test/session-client.ts && node --experimental-strip-types test/ui-commands.ts && node --experimental-strip-types test/ui-request.ts && node --experimental-strip-types test/provider-auth.ts && node --experimental-strip-types test/session-actions.ts && node --experimental-strip-types test/navigation.ts && node --experimental-strip-types test/api-client.ts && esbuild test/compare.ts --bundle --platform=node --format=cjs --outfile=node_modules/.tmp/compare.cjs --loader:.tsx=tsx --jsx=automatic --log-level=error && node node_modules/.tmp/compare.cjs && node --experimental-strip-types test/runs.ts && node --experimental-strip-types test/workspaces.ts && node --experimental-strip-types test/chat-lifecycle.ts && node --experimental-strip-types test/workspace-api.ts && node --experimental-strip-types test/api.ts && node --experimental-strip-types test/history-api.ts && node --experimental-strip-types test/stop-api.ts && node --experimental-strip-types test/cli.ts && node --experimental-strip-types test/pi-startup.ts",
40
+ "test": "node --experimental-strip-types test/ui-compaction.ts && node --experimental-strip-types test/session-client.ts && node --experimental-strip-types test/ui-commands.ts && node --experimental-strip-types test/ui-request.ts && node --experimental-strip-types test/provider-auth.ts && node --experimental-strip-types test/session-actions.ts && node --experimental-strip-types test/navigation.ts && node --experimental-strip-types test/api-client.ts && esbuild test/compare.ts --bundle --platform=node --format=cjs --outfile=node_modules/.tmp/compare.cjs --loader:.tsx=tsx --jsx=automatic --log-level=error && node node_modules/.tmp/compare.cjs && node --experimental-strip-types test/runs.ts && node --experimental-strip-types test/workspaces.ts && node --experimental-strip-types test/chat-lifecycle.ts && node --experimental-strip-types test/workspace-api.ts && node --experimental-strip-types test/api.ts && node --experimental-strip-types test/history-api.ts && node --experimental-strip-types test/stop-api.ts && node --experimental-strip-types test/cli.ts && node --experimental-strip-types test/pi-startup.ts && node test/pi-default-extension.mjs",
41
41
  "test:browser": "node test/url-browser.mjs && node test/workspace-browser.mjs && node test/pi-dialog-browser.mjs && node test/stop-browser.mjs && node test/session-actions-browser.mjs"
42
42
  },
43
43
  "dependencies": {