@diologue/local-agent 0.1.5 → 0.2.0
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/dist/cli.mjs +20 -1
- package/dist/cli.mjs.map +3 -3
- package/package.json +3 -5
- package/scripts/postinstall.ts +0 -191
- package/src/lib/engine-bundle.ts +0 -123
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diologue/local-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Diologue Coding Agent — local helper that pairs the cloud web UI with opencode running against a git repo on your machine. Binds to 127.0.0.1 only.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
-
"scripts/postinstall.ts",
|
|
18
|
-
"src/lib/engine-bundle.ts",
|
|
19
17
|
"engine-release.json",
|
|
20
18
|
"README.md"
|
|
21
19
|
],
|
|
@@ -30,11 +28,11 @@
|
|
|
30
28
|
"check": "tsc --noEmit",
|
|
31
29
|
"test": "tsx --test src/*.test.ts src/**/*.test.ts",
|
|
32
30
|
"verify-sdk": "tsx src/scripts/verify-sdk.ts",
|
|
33
|
-
"pair": "tsx src/cli.ts pair"
|
|
34
|
-
"postinstall": "node dist/postinstall.mjs || true"
|
|
31
|
+
"pair": "tsx src/cli.ts pair"
|
|
35
32
|
},
|
|
36
33
|
"dependencies": {
|
|
37
34
|
"@opencode-ai/sdk": "^1.15.13",
|
|
35
|
+
"opencode-ai": "1.16.2",
|
|
38
36
|
"express": "^4.21.2",
|
|
39
37
|
"open": "^10.1.0",
|
|
40
38
|
"zod": "^3.24.2"
|
package/scripts/postinstall.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
// local-agent postinstall — runs after `npm install @diologue/local-agent`.
|
|
2
|
-
//
|
|
3
|
-
// Responsibility: download the diologue-engine bundle for this host's
|
|
4
|
-
// platform/arch from GitHub Releases and write it under
|
|
5
|
-
// local-agent/dist/engine/<releaseTag>/.
|
|
6
|
-
//
|
|
7
|
-
// Behavior:
|
|
8
|
-
// - Best-effort. Failures here should NEVER block the install — the
|
|
9
|
-
// helper can still run via LOCAL_AGENT_ENGINE=npm, and the runtime
|
|
10
|
-
// surfaces a clear error if no engine is reachable. We print a
|
|
11
|
-
// loud warning instead of throwing.
|
|
12
|
-
// - Honours SKIP_DIOLOGUE_POSTINSTALL=1 for CI and offline setups.
|
|
13
|
-
// - Idempotent: if the target file already exists, exits early.
|
|
14
|
-
// - Unsupported platforms (e.g. freebsd) print a one-line warning
|
|
15
|
-
// and exit 0.
|
|
16
|
-
//
|
|
17
|
-
// Source of truth for what to fetch: local-agent/engine-release.json.
|
|
18
|
-
|
|
19
|
-
import { chmod, mkdir, rename, stat, writeFile } from "node:fs/promises";
|
|
20
|
-
import { createWriteStream } from "node:fs";
|
|
21
|
-
import { pipeline } from "node:stream/promises";
|
|
22
|
-
import { Readable } from "node:stream";
|
|
23
|
-
import path from "node:path";
|
|
24
|
-
import { fileURLToPath } from "node:url";
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
BUNDLE_DIR_INSTALLED,
|
|
28
|
-
bundleFilename,
|
|
29
|
-
downloadUrlForHost,
|
|
30
|
-
engineRelease,
|
|
31
|
-
supportedTargetForHost,
|
|
32
|
-
} from "../src/lib/engine-bundle";
|
|
33
|
-
|
|
34
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
-
const __dirname = path.dirname(__filename);
|
|
36
|
-
|
|
37
|
-
const SKIP_ENV = "SKIP_DIOLOGUE_POSTINSTALL" as const;
|
|
38
|
-
|
|
39
|
-
const log = (msg: string): void => {
|
|
40
|
-
// Prefix consistently so users can grep their npm-install log.
|
|
41
|
-
process.stdout.write(`[diologue/postinstall] ${msg}\n`);
|
|
42
|
-
};
|
|
43
|
-
const warn = (msg: string): void => {
|
|
44
|
-
process.stderr.write(`[diologue/postinstall] ${msg}\n`);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export interface PostinstallOptions {
|
|
48
|
-
/** Where to put the downloaded bundle. Defaults to BUNDLE_DIR_INSTALLED. */
|
|
49
|
-
destinationDir?: string;
|
|
50
|
-
/** Override for the fetch function. Tests inject a mock. */
|
|
51
|
-
fetchImpl?: typeof fetch;
|
|
52
|
-
/** Force a re-download even if the file already exists. */
|
|
53
|
-
force?: boolean;
|
|
54
|
-
/** Skip the actual write (dry-run). */
|
|
55
|
-
dryRun?: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface PostinstallResult {
|
|
59
|
-
status: "installed" | "already-present" | "skipped" | "unsupported" | "failed";
|
|
60
|
-
/** Reason for skipped/unsupported/failed. */
|
|
61
|
-
reason?: string;
|
|
62
|
-
/** Path to the installed bundle (when status is installed or already-present). */
|
|
63
|
-
bundlePath?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const runPostinstall = async (
|
|
67
|
-
options: PostinstallOptions = {},
|
|
68
|
-
): Promise<PostinstallResult> => {
|
|
69
|
-
if (process.env[SKIP_ENV] === "1") {
|
|
70
|
-
return { status: "skipped", reason: `${SKIP_ENV}=1 set` };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const target = supportedTargetForHost();
|
|
74
|
-
if (!target) {
|
|
75
|
-
return {
|
|
76
|
-
status: "unsupported",
|
|
77
|
-
reason: `No bundle for ${process.platform}/${process.arch}. ` +
|
|
78
|
-
`Supported: ${engineRelease.targets
|
|
79
|
-
.map((t) => `${t.platform}/${t.arch}`)
|
|
80
|
-
.join(", ")}. ` +
|
|
81
|
-
`Helper will still work via LOCAL_AGENT_ENGINE=npm.`,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const url = downloadUrlForHost()!;
|
|
86
|
-
const destinationDir = options.destinationDir ?? BUNDLE_DIR_INSTALLED;
|
|
87
|
-
const destination = path.join(destinationDir, bundleFilename());
|
|
88
|
-
|
|
89
|
-
if (!options.force) {
|
|
90
|
-
try {
|
|
91
|
-
const s = await stat(destination);
|
|
92
|
-
if (s.isFile() && s.size > 0) {
|
|
93
|
-
return {
|
|
94
|
-
status: "already-present",
|
|
95
|
-
bundlePath: destination,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// not present — fall through to download
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (options.dryRun) {
|
|
104
|
-
return {
|
|
105
|
-
status: "installed",
|
|
106
|
-
bundlePath: destination,
|
|
107
|
-
reason: `dry-run: would fetch ${url}`,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
112
|
-
let response: Response;
|
|
113
|
-
try {
|
|
114
|
-
response = await fetchImpl(url);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
return {
|
|
117
|
-
status: "failed",
|
|
118
|
-
reason: `Network error fetching ${url}: ${(err as Error).message}`,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
if (!response.ok || !response.body) {
|
|
122
|
-
return {
|
|
123
|
-
status: "failed",
|
|
124
|
-
reason: `HTTP ${response.status} fetching ${url}. ` +
|
|
125
|
-
`If the engine release isn't published yet, set ` +
|
|
126
|
-
`LOCAL_AGENT_ENGINE=npm to use the legacy MVP path.`,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
await mkdir(destinationDir, { recursive: true });
|
|
131
|
-
const tmp = destination + ".part";
|
|
132
|
-
try {
|
|
133
|
-
await pipeline(
|
|
134
|
-
// Node's fetch returns a Web ReadableStream; convert to Node stream.
|
|
135
|
-
Readable.fromWeb(response.body as never),
|
|
136
|
-
createWriteStream(tmp),
|
|
137
|
-
);
|
|
138
|
-
await chmod(tmp, 0o755);
|
|
139
|
-
await rename(tmp, destination);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
return {
|
|
142
|
-
status: "failed",
|
|
143
|
-
reason: `Write error: ${(err as Error).message}`,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { status: "installed", bundlePath: destination };
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// ---- CLI ------------------------------------------------------------
|
|
151
|
-
|
|
152
|
-
const isMain =
|
|
153
|
-
process.argv[1] !== undefined &&
|
|
154
|
-
fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
155
|
-
|
|
156
|
-
if (isMain) {
|
|
157
|
-
runPostinstall()
|
|
158
|
-
.then((result) => {
|
|
159
|
-
switch (result.status) {
|
|
160
|
-
case "installed":
|
|
161
|
-
log(`installed → ${result.bundlePath}`);
|
|
162
|
-
break;
|
|
163
|
-
case "already-present":
|
|
164
|
-
log(`already present → ${result.bundlePath}`);
|
|
165
|
-
break;
|
|
166
|
-
case "skipped":
|
|
167
|
-
log(`skipped: ${result.reason ?? "no reason given"}`);
|
|
168
|
-
break;
|
|
169
|
-
case "unsupported":
|
|
170
|
-
warn(result.reason ?? "unsupported");
|
|
171
|
-
break;
|
|
172
|
-
case "failed":
|
|
173
|
-
warn(`bundle install failed — ${result.reason ?? "unknown"}`);
|
|
174
|
-
warn(
|
|
175
|
-
"The helper will still install. " +
|
|
176
|
-
"Set LOCAL_AGENT_ENGINE=npm to use the legacy MVP path, " +
|
|
177
|
-
"or re-run the install once the engine release is published.",
|
|
178
|
-
);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
.catch((err: Error) => {
|
|
183
|
-
// Unexpected — log and exit 0 anyway so we don't break npm install.
|
|
184
|
-
warn(`unexpected failure: ${err.message}`);
|
|
185
|
-
warn("Continuing without bundle. Set LOCAL_AGENT_ENGINE=npm to use the legacy path.");
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Suppress unused-var warning when this file is consumed only as a CLI.
|
|
190
|
-
void __dirname;
|
|
191
|
-
void writeFile;
|
package/src/lib/engine-bundle.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// Engine-bundle discovery — finds the standalone diologue-engine binary
|
|
2
|
-
// that postinstall (or `npm run build-engine-bundle`) produced on this
|
|
3
|
-
// machine.
|
|
4
|
-
//
|
|
5
|
-
// Discovery order (first hit wins):
|
|
6
|
-
// 1. LOCAL_AGENT_ENGINE_BUNDLE env var — explicit override (tests + dev)
|
|
7
|
-
// 2. <local-agent>/dist/engine/<releaseTag>/diologue-engine-<plat>-<arch><ext>
|
|
8
|
-
// — where postinstall.ts writes the downloaded bundle
|
|
9
|
-
// 3. <repo>/build/diologue-engine-bundles/diologue-engine-<plat>-<arch><ext>
|
|
10
|
-
// — where build-engine-bundle.ts writes a locally-built bundle
|
|
11
|
-
//
|
|
12
|
-
// Returning null means "no bundle on this machine"; the caller decides
|
|
13
|
-
// whether to fall back to a different engine path or surface an error.
|
|
14
|
-
|
|
15
|
-
import { access, constants } from "node:fs/promises";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
import engineReleaseRaw from "../../engine-release.json" with { type: "json" };
|
|
20
|
-
|
|
21
|
-
interface EngineRelease {
|
|
22
|
-
version: string;
|
|
23
|
-
releaseTag: string;
|
|
24
|
-
downloadBase: string;
|
|
25
|
-
targets: Array<{
|
|
26
|
-
platform: NodeJS.Platform;
|
|
27
|
-
arch: NodeJS.Architecture;
|
|
28
|
-
bunTarget: string;
|
|
29
|
-
ext: string;
|
|
30
|
-
}>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export const engineRelease = engineReleaseRaw as EngineRelease;
|
|
34
|
-
|
|
35
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
36
|
-
const __dirname = path.dirname(__filename);
|
|
37
|
-
|
|
38
|
-
// local-agent/src/lib → local-agent root is two levels up.
|
|
39
|
-
const LOCAL_AGENT_ROOT = path.resolve(__dirname, "../..");
|
|
40
|
-
// local-agent → repo root is one level up.
|
|
41
|
-
const REPO_ROOT = path.resolve(LOCAL_AGENT_ROOT, "..");
|
|
42
|
-
|
|
43
|
-
export const BUNDLE_DIR_INSTALLED = path.join(
|
|
44
|
-
LOCAL_AGENT_ROOT,
|
|
45
|
-
"dist/engine",
|
|
46
|
-
engineRelease.releaseTag,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
export const BUNDLE_DIR_LOCAL_BUILD = path.join(
|
|
50
|
-
REPO_ROOT,
|
|
51
|
-
"build/diologue-engine-bundles",
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
export const ENGINE_BUNDLE_ENV = "LOCAL_AGENT_ENGINE_BUNDLE" as const;
|
|
55
|
-
|
|
56
|
-
export interface ResolvedBundle {
|
|
57
|
-
/** Absolute path to the executable. */
|
|
58
|
-
path: string;
|
|
59
|
-
/** Where it came from — useful for logging + tests. */
|
|
60
|
-
source: "env" | "installed" | "local-build";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const exists = async (p: string): Promise<boolean> => {
|
|
64
|
-
try {
|
|
65
|
-
await access(p, constants.X_OK);
|
|
66
|
-
return true;
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
/** Build the canonical bundle filename for a given target. The
|
|
73
|
-
* postinstall script uses this name to write the artifact; the build
|
|
74
|
-
* script writes the same name. */
|
|
75
|
-
export const bundleFilename = (
|
|
76
|
-
platform: NodeJS.Platform = process.platform,
|
|
77
|
-
arch: NodeJS.Architecture = process.arch,
|
|
78
|
-
): string => {
|
|
79
|
-
const target = engineRelease.targets.find(
|
|
80
|
-
(t) => t.platform === platform && t.arch === arch,
|
|
81
|
-
);
|
|
82
|
-
const ext = target?.ext ?? "";
|
|
83
|
-
return `diologue-engine-${platform}-${arch}${ext}`;
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/** Return the bundle target for this host, or undefined if unsupported. */
|
|
87
|
-
export const supportedTargetForHost = ():
|
|
88
|
-
| EngineRelease["targets"][number]
|
|
89
|
-
| undefined => {
|
|
90
|
-
return engineRelease.targets.find(
|
|
91
|
-
(t) => t.platform === process.platform && t.arch === process.arch,
|
|
92
|
-
);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** Search for an engine bundle on the local machine. Returns null
|
|
96
|
-
* when nothing is found. */
|
|
97
|
-
export const findEngineBundle = async (): Promise<ResolvedBundle | null> => {
|
|
98
|
-
const fromEnv = process.env[ENGINE_BUNDLE_ENV];
|
|
99
|
-
if (fromEnv) {
|
|
100
|
-
if (await exists(fromEnv)) {
|
|
101
|
-
return { path: fromEnv, source: "env" };
|
|
102
|
-
}
|
|
103
|
-
// env var set but file missing — fall through so the caller still
|
|
104
|
-
// gets a useful "not found" rather than a half-broken path.
|
|
105
|
-
}
|
|
106
|
-
const filename = bundleFilename();
|
|
107
|
-
const installed = path.join(BUNDLE_DIR_INSTALLED, filename);
|
|
108
|
-
if (await exists(installed)) {
|
|
109
|
-
return { path: installed, source: "installed" };
|
|
110
|
-
}
|
|
111
|
-
const localBuild = path.join(BUNDLE_DIR_LOCAL_BUILD, filename);
|
|
112
|
-
if (await exists(localBuild)) {
|
|
113
|
-
return { path: localBuild, source: "local-build" };
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/** Compose the download URL for the engine bundle on this host. */
|
|
119
|
-
export const downloadUrlForHost = (): string | null => {
|
|
120
|
-
const target = supportedTargetForHost();
|
|
121
|
-
if (!target) return null;
|
|
122
|
-
return `${engineRelease.downloadBase}/${engineRelease.releaseTag}/${bundleFilename()}`;
|
|
123
|
-
};
|