@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 +5 -0
- package/bin/pi-default-extension.mjs +47 -0
- package/bin/pi-rpc.mjs +14 -0
- package/dist/server.mjs +2 -1
- package/package.json +2 -2
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.
|
|
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": {
|