litopencode 0.0.3 → 0.0.4

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
@@ -34,9 +34,9 @@
34
34
 
35
35
  npx litopencode install
36
36
 
37
- The installer delegates the default install path to OpenCode's own plugin installer and registers a version-pinned entry such as <code>litopencode@0.0.3</code>. Then restart OpenCode. The agent switcher should show <code>lit-plan</code> and <code>lit-loop</code>.
37
+ The installer delegates the default install path to OpenCode's own plugin installer. Published installs register a version-pinned entry such as <code>litopencode@0.0.4</code>; local checkout installs register the local package path for development. Then restart OpenCode. The agent switcher should show <code>lit-plan</code> and <code>lit-loop</code>.
38
38
 
39
- For a preview without writing <code>opencode.json</code>:
39
+ For a preview without writing OpenCode config:
40
40
 
41
41
  npx litopencode install --dry-run
42
42
 
@@ -44,7 +44,7 @@ For health checks:
44
44
 
45
45
  npx litopencode doctor
46
46
 
47
- By default, the installer targets <code>~/.config/opencode/opencode.json</code>, or <code>$XDG_CONFIG_HOME/opencode/opencode.json</code> when <code>XDG_CONFIG_HOME</code> is set. Use <code>--root &lt;dir&gt;</code> for a custom OpenCode config directory.
47
+ By default, OpenCode manages <code>~/.config/opencode/opencode.jsonc</code>, or <code>$XDG_CONFIG_HOME/opencode/opencode.jsonc</code> when <code>XDG_CONFIG_HOME</code> is set. Use <code>--root &lt;dir&gt;</code> for a custom OpenCode config directory; custom roots use a direct <code>opencode.json</code> patch for deterministic tests and previews.
48
48
 
49
49
  ### Local Source Probe
50
50
 
@@ -11,20 +11,37 @@ function toolsToConfig(tools) {
11
11
  }
12
12
  return enabledTools;
13
13
  }
14
+ function toolsToPermission(tools) {
15
+ const permission = {};
16
+ if (tools.includes("edit") || tools.includes("write"))
17
+ permission.edit = "allow";
18
+ if (tools.includes("bash"))
19
+ permission.bash = "allow";
20
+ if (tools.includes("webfetch"))
21
+ permission.webfetch = "allow";
22
+ return Object.keys(permission).length > 0 ? permission : undefined;
23
+ }
14
24
  export function toOpenCodeAgentConfig(agent) {
15
25
  return {
16
26
  description: agent.summary,
17
27
  prompt: agent.prompt,
18
- mode: agent.mode,
28
+ mode: agent.mode === "primary" ? "all" : "subagent",
19
29
  tools: toolsToConfig(agent.tools),
20
30
  color: agent.color,
21
- maxSteps: agent.maxSteps
31
+ maxSteps: agent.maxSteps,
32
+ ...(agent.mode === "subagent" ? { hidden: true } : {}),
33
+ ...(toolsToPermission(agent.tools) ? { permission: toolsToPermission(agent.tools) } : {})
22
34
  };
23
35
  }
24
36
  export function registerLitOpenCodeAgents(config) {
37
+ const existingBuild = config.agent?.build ?? {};
38
+ const existingPlan = config.agent?.plan ?? {};
25
39
  config.agent = {
26
- ...config.agent
40
+ ...config.agent,
41
+ build: { ...existingBuild, mode: "subagent", hidden: true },
42
+ plan: { ...existingPlan, mode: "subagent", hidden: true }
27
43
  };
44
+ config.default_agent = "lit-loop";
28
45
  for (const agent of litOpenCodeAgents) {
29
46
  config.agent[agent.id] = toOpenCodeAgentConfig(agent);
30
47
  }
@@ -1,7 +1,9 @@
1
1
  import type { Config } from "@opencode-ai/plugin";
2
2
  export type AgentTier = "default" | "role" | "specialist";
3
3
  export type AgentMode = "primary" | "subagent";
4
+ export type OpenCodeAgentMode = "all" | "subagent";
4
5
  export type AgentToolId = "read" | "write" | "edit" | "bash" | "webfetch" | "grep";
6
+ export type AgentPermission = "ask" | "allow" | "deny";
5
7
  export type LitOpenCodeAgent = {
6
8
  readonly id: string;
7
9
  readonly name: string;
@@ -18,12 +20,20 @@ export type LitOpenCodeAgent = {
18
20
  export type OpenCodeAgentConfig = {
19
21
  readonly description: string;
20
22
  readonly prompt: string;
21
- readonly mode: AgentMode;
23
+ readonly mode: OpenCodeAgentMode;
22
24
  readonly tools: Record<string, boolean>;
23
25
  readonly color: string;
24
26
  readonly maxSteps: number;
27
+ readonly hidden?: boolean;
28
+ readonly permission?: {
29
+ readonly edit?: AgentPermission;
30
+ readonly bash?: AgentPermission;
31
+ readonly webfetch?: AgentPermission;
32
+ };
33
+ };
34
+ export type AgentConfigTarget = Pick<Config, "agent"> & {
35
+ default_agent?: string;
25
36
  };
26
- export type AgentConfigTarget = Pick<Config, "agent">;
27
37
  export declare const planningTools: readonly ("read" | "grep")[];
28
38
  export declare const workerTools: readonly ("read" | "write" | "edit" | "bash" | "grep")[];
29
39
  export declare const reviewTools: readonly ("read" | "bash" | "grep")[];
package/dist/cli/args.js CHANGED
@@ -49,7 +49,7 @@ export function helpText() {
49
49
  " litopencode doctor [--root <dir>]",
50
50
  "",
51
51
  "Commands:",
52
- " install Add litopencode to opencode.json with a branded installer UI.",
52
+ " install Register litopencode with OpenCode using a branded installer UI.",
53
53
  " doctor Report package, config, and runtime path status without writing files.",
54
54
  "",
55
55
  "Default root:",
@@ -37,20 +37,98 @@ function defaultOpenCodeRoot() {
37
37
  function shouldUseOpenCodeInstaller(root) {
38
38
  return path.resolve(root) === path.resolve(defaultOpenCodeRoot());
39
39
  }
40
+ function openCodeConfigCandidates(root) {
41
+ const resolvedRoot = path.resolve(root);
42
+ return [path.join(resolvedRoot, "opencode.json"), path.join(resolvedRoot, "opencode.jsonc")];
43
+ }
44
+ async function openCodeManagedConfigFile(root) {
45
+ const [jsonFile, jsoncFile] = openCodeConfigCandidates(root);
46
+ if (await pathExists(jsonFile))
47
+ return jsonFile;
48
+ if (await pathExists(jsoncFile))
49
+ return jsoncFile;
50
+ return jsoncFile;
51
+ }
52
+ function outputText(value) {
53
+ if (typeof value === "string")
54
+ return value;
55
+ if (value instanceof Uint8Array)
56
+ return Buffer.from(value).toString("utf8");
57
+ return "";
58
+ }
59
+ function stripAnsi(value) {
60
+ return value.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "");
61
+ }
62
+ function commandFailureDetail(error) {
63
+ if (!(error instanceof Error))
64
+ return String(error);
65
+ const failure = error;
66
+ const parts = [];
67
+ const stderr = stripAnsi(outputText(failure.stderr)).trim();
68
+ const stdout = stripAnsi(outputText(failure.stdout)).trim();
69
+ const code = typeof failure.code === "number" || typeof failure.code === "string" ? String(failure.code) : "";
70
+ const signal = typeof failure.signal === "string" ? failure.signal : "";
71
+ if (stderr.length > 0)
72
+ parts.push("stderr:\n" + stderr);
73
+ if (stdout.length > 0)
74
+ parts.push("stdout:\n" + stdout);
75
+ if (code.length > 0)
76
+ parts.push("exit code: " + code);
77
+ if (signal.length > 0)
78
+ parts.push("signal: " + signal);
79
+ if (parts.length === 0 && error.message.length > 0)
80
+ parts.push(error.message);
81
+ return parts.join("\n\n");
82
+ }
83
+ async function pathExists(filePath) {
84
+ try {
85
+ await fs.stat(filePath);
86
+ return true;
87
+ }
88
+ catch (error) {
89
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
90
+ return false;
91
+ throw error;
92
+ }
93
+ }
94
+ async function isLocalSourcePackage(packageRoot) {
95
+ const realPackageRoot = await fs.realpath(packageRoot);
96
+ return (await pathExists(path.join(realPackageRoot, ".git"))) && (await pathExists(path.join(realPackageRoot, "src")));
97
+ }
98
+ async function openCodeInstallTarget(metadata) {
99
+ if (await isLocalSourcePackage(metadata.packageRoot)) {
100
+ const realPackageRoot = await fs.realpath(metadata.packageRoot);
101
+ return { value: realPackageRoot, label: metadata.name + "@" + metadata.version + " (local checkout)" };
102
+ }
103
+ const spec = pluginSpec(metadata);
104
+ return { value: spec, label: spec };
105
+ }
40
106
  async function installWithOpenCode(target) {
41
107
  try {
42
- await execFileAsync("opencode", ["plugin", target, "--global", "--force"], {
108
+ await execFileAsync("opencode", ["plugin", target.value, "--global", "--force"], {
43
109
  timeout: 120_000,
44
110
  maxBuffer: 1024 * 1024 * 4
45
111
  });
46
112
  }
47
113
  catch (error) {
48
- const detail = error instanceof Error && "stderr" in error && typeof error.stderr === "string"
49
- ? error.stderr.trim()
50
- : error instanceof Error
51
- ? error.message
52
- : String(error);
53
- throw new Error("OpenCode plugin install failed for " + target + (detail ? ":\n" + detail : ""));
114
+ const detail = commandFailureDetail(error);
115
+ throw new Error("OpenCode plugin install failed for " + target.label + (detail ? ":\n" + detail : ""));
116
+ }
117
+ }
118
+ async function removeStaleOpenCodeEntries(root, target) {
119
+ for (const configFile of openCodeConfigCandidates(root)) {
120
+ const config = await readJsonObjectIfPresent(configFile);
121
+ const pluginValue = config?.plugin;
122
+ if (!Array.isArray(pluginValue))
123
+ continue;
124
+ const nextPlugin = pluginValue.filter((entry) => {
125
+ if (entry === target.value)
126
+ return true;
127
+ return !isLitOpenCodeEntry(entry);
128
+ });
129
+ if (nextPlugin.length === pluginValue.length)
130
+ continue;
131
+ await fs.writeFile(configFile, JSON.stringify({ ...config, plugin: nextPlugin }, null, 2) + "\n");
54
132
  }
55
133
  }
56
134
  function describePluginMutation(config, target) {
@@ -135,25 +213,29 @@ function renderInstallReport(report) {
135
213
  export async function install(root, dryRun) {
136
214
  const metadata = await readPackageMetadata();
137
215
  const paths = createRuntimePaths(root);
138
- const before = await readJsonObjectIfPresent(paths.opencodeConfigFile);
139
- const target = pluginSpec(metadata);
140
- const mutation = describePluginMutation(before, target);
216
+ const useOpenCodeInstaller = shouldUseOpenCodeInstaller(root);
217
+ const configFile = useOpenCodeInstaller ? await openCodeManagedConfigFile(root) : paths.opencodeConfigFile;
218
+ const before = useOpenCodeInstaller ? null : await readJsonObjectIfPresent(paths.opencodeConfigFile);
219
+ const spec = pluginSpec(metadata);
220
+ const target = useOpenCodeInstaller ? await openCodeInstallTarget(metadata) : { value: spec, label: spec };
221
+ const mutation = describePluginMutation(before, target.value);
141
222
  const report = {
142
223
  dryRun,
143
- path: paths.opencodeConfigFile,
224
+ path: configFile,
144
225
  plugin: mutation.plugin,
145
226
  patch: mutation.patch,
146
227
  changed: mutation.changed,
147
- package: metadata
228
+ package: { name: metadata.name, version: metadata.version }
148
229
  };
149
230
  if (dryRun) {
150
231
  return { exitCode: 0, stdout: JSON.stringify(report, null, 2) };
151
232
  }
152
- if (shouldUseOpenCodeInstaller(root)) {
233
+ if (useOpenCodeInstaller) {
234
+ await removeStaleOpenCodeEntries(root, target);
153
235
  await installWithOpenCode(target);
154
236
  }
155
237
  else if (mutation.changed) {
156
- const next = applyPluginMutation(before, mutation, target);
238
+ const next = applyPluginMutation(before, mutation, target.value);
157
239
  await fs.mkdir(path.dirname(paths.opencodeConfigFile), { recursive: true });
158
240
  await fs.writeFile(paths.opencodeConfigFile, JSON.stringify(next, null, 2) + "\n");
159
241
  }
package/dist/cli/json.js CHANGED
@@ -5,12 +5,13 @@ export function isRecord(value) {
5
5
  return typeof value === "object" && value !== null && !Array.isArray(value);
6
6
  }
7
7
  export async function readPackageMetadata() {
8
- const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
+ const packagePath = path.join(packageRoot, "package.json");
9
10
  const parsed = JSON.parse(await fs.readFile(packagePath, "utf8"));
10
11
  if (!isRecord(parsed) || typeof parsed.name !== "string" || typeof parsed.version !== "string") {
11
12
  throw new Error(`Malformed package metadata at ${packagePath}`);
12
13
  }
13
- return { name: parsed.name, version: parsed.version };
14
+ return { name: parsed.name, version: parsed.version, packageRoot };
14
15
  }
15
16
  export async function readJsonObjectIfPresent(filePath) {
16
17
  let raw;
@@ -6,6 +6,7 @@ export type CliResult = {
6
6
  export type PackageMetadata = {
7
7
  readonly name: string;
8
8
  readonly version: string;
9
+ readonly packageRoot: string;
9
10
  };
10
11
  export type ParsedArgs = {
11
12
  readonly command?: string;
@@ -45,5 +46,5 @@ export type InstallReport = {
45
46
  readonly plugin: PluginMutation["plugin"];
46
47
  readonly patch: readonly JsonPatchOperation[];
47
48
  readonly changed: boolean;
48
- readonly package: PackageMetadata;
49
+ readonly package: Pick<PackageMetadata, "name" | "version">;
49
50
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Hooks, PluginInput } from "@opencode-ai/plugin";
1
+ import litOpenCodePlugin from "./server.ts";
2
2
  export { appendLedgerEvent, createLitGoalOperations, initializeLitGoal, readLedgerEvents, recoverLedgerTemps, LedgerIoError, LedgerParseError, type JsonValue, type LedgerAppendResult, type LedgerEvent, type LedgerRecoveryReport, type LitGoalInitialization, type LitGoalOperations } from "./ledger.ts";
3
3
  export { litActivationBanner, litOpenCodeCommands } from "./commands.ts";
4
4
  export { createToolExecuteAfterHook, createToolExecuteBeforeHook } from "./hooks.ts";
@@ -7,9 +7,8 @@ export { litOpenCodeTools, litTool, litworkTool } from "./tools.ts";
7
7
  export { findLitOpenCodeFeature, litOpenCodeFeatures, type LitOpenCodeBindingKind, type LitOpenCodeFeature, type LitOpenCodeFeatureBinding, type LitOpenCodeFeatureId } from "./features.ts";
8
8
  export { findLitOpenCodeRuntimeSkill, litOpenCodeRuntimeSkills, type LitOpenCodeRuntimeSkill, type LitOpenCodeRuntimeSkillId } from "./skills.ts";
9
9
  export declare const pluginId = "litopencode";
10
- declare const litOpenCodePlugin: (input?: PluginInput) => Promise<Hooks>;
11
10
  export declare const pluginModule: {
12
11
  id: string;
13
- server: (input?: PluginInput) => Promise<Hooks>;
12
+ server: (input?: import("@opencode-ai/plugin").PluginInput) => Promise<import("@opencode-ai/plugin").Hooks>;
14
13
  };
15
14
  export default litOpenCodePlugin;
package/dist/index.js CHANGED
@@ -1,10 +1,5 @@
1
- import { registerLitOpenCodeAgents } from "./agents.js";
2
- import { createCommandActivationHook, litActivationBanner, litOpenCodeCommands } from "./commands.js";
3
- import { loadConfig } from "./config.js";
4
- import { createToolExecuteAfterHook, createToolExecuteBeforeHook } from "./hooks.js";
1
+ import litOpenCodePlugin from "./server.js";
5
2
  export { appendLedgerEvent, createLitGoalOperations, initializeLitGoal, readLedgerEvents, recoverLedgerTemps, LedgerIoError, LedgerParseError } from "./ledger.js";
6
- import { createLogger } from "./logger.js";
7
- import { litOpenCodeTools } from "./tools.js";
8
3
  export { litActivationBanner, litOpenCodeCommands } from "./commands.js";
9
4
  export { createToolExecuteAfterHook, createToolExecuteBeforeHook } from "./hooks.js";
10
5
  export { applyLitOpenCodeToolAfterHook, applyLitOpenCodeToolBeforeHook } from "./tool-guards.js";
@@ -12,32 +7,6 @@ export { litOpenCodeTools, litTool, litworkTool } from "./tools.js";
12
7
  export { findLitOpenCodeFeature, litOpenCodeFeatures } from "./features.js";
13
8
  export { findLitOpenCodeRuntimeSkill, litOpenCodeRuntimeSkills } from "./skills.js";
14
9
  export const pluginId = "litopencode";
15
- const litOpenCodePlugin = async (input) => {
16
- const root = input?.worktree ?? input?.directory ?? ".";
17
- const loaded = await loadConfig(root);
18
- const logger = createLogger(loaded.paths);
19
- const hooks = {
20
- config: async (config) => {
21
- registerLitOpenCodeAgents(config);
22
- },
23
- tool: litOpenCodeTools,
24
- "command.execute.before": createCommandActivationHook(root),
25
- dispose: async () => {
26
- await logger.dispose();
27
- }
28
- };
29
- Object.defineProperties(hooks, {
30
- "tool.execute.before": {
31
- value: createToolExecuteBeforeHook(),
32
- enumerable: false
33
- },
34
- "tool.execute.after": {
35
- value: createToolExecuteAfterHook(),
36
- enumerable: false
37
- }
38
- });
39
- return hooks;
40
- };
41
10
  export const pluginModule = {
42
11
  id: pluginId,
43
12
  server: litOpenCodePlugin
@@ -0,0 +1,3 @@
1
+ import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
+ declare const litOpenCodePlugin: (input?: PluginInput) => Promise<Hooks>;
3
+ export default litOpenCodePlugin;
package/dist/server.js ADDED
@@ -0,0 +1,33 @@
1
+ import { registerLitOpenCodeAgents } from "./agents.js";
2
+ import { createCommandActivationHook } from "./commands.js";
3
+ import { loadConfig } from "./config.js";
4
+ import { createToolExecuteAfterHook, createToolExecuteBeforeHook } from "./hooks.js";
5
+ import { createLogger } from "./logger.js";
6
+ import { litOpenCodeTools } from "./tools.js";
7
+ const litOpenCodePlugin = async (input) => {
8
+ const root = input?.worktree ?? input?.directory ?? ".";
9
+ const loaded = await loadConfig(root);
10
+ const logger = createLogger(loaded.paths);
11
+ const hooks = {
12
+ config: async (config) => {
13
+ registerLitOpenCodeAgents(config);
14
+ },
15
+ tool: litOpenCodeTools,
16
+ "command.execute.before": createCommandActivationHook(root),
17
+ dispose: async () => {
18
+ await logger.dispose();
19
+ }
20
+ };
21
+ Object.defineProperties(hooks, {
22
+ "tool.execute.before": {
23
+ value: createToolExecuteBeforeHook(),
24
+ enumerable: false
25
+ },
26
+ "tool.execute.after": {
27
+ value: createToolExecuteAfterHook(),
28
+ enumerable: false
29
+ }
30
+ });
31
+ return hooks;
32
+ };
33
+ export default litOpenCodePlugin;
package/dist/skills.js CHANGED
@@ -40,6 +40,7 @@ export const litOpenCodeRuntimeSkills = Object.freeze([
40
40
  discovery: "Run npx litopencode install, litopencode doctor, or litopencode install --dry-run.",
41
41
  safety: [
42
42
  "Default installation delegates to the OpenCode plugin installer; custom roots write only the version-pinned opencode.json plugin entry.",
43
+ "Local checkout installs hand OpenCode the package path so unpublished versions can be tested before npm publish.",
43
44
  "Malformed config fails closed with a typed config error."
44
45
  ]
45
46
  },
package/docs/migration.md CHANGED
@@ -11,9 +11,9 @@ This guide describes the supported migration shape for moving an OpenCode workfl
11
11
  ## Environment And Config
12
12
 
13
13
  - Use the `LITOPENCODE_` prefix for LitOpenCode-owned environment variables.
14
- - Keep OpenCode plugin configuration in `opencode.json`.
14
+ - Keep default OpenCode plugin configuration in OpenCode's managed `opencode.jsonc`; custom-root LitOpenCode previews use `opencode.json`.
15
15
  - Keep LitOpenCode runtime config in `.litopencode/config.json` when project-local config is needed.
16
- - Use `npx litopencode install` to delegate default setup to `opencode plugin litopencode@0.0.3 --global --force` and register the version-pinned plugin entry.
16
+ - Use `npx litopencode install` to delegate default setup to `opencode plugin <target> --global --force`; published installs use `litopencode@0.0.4`, while local checkout installs use the package path.
17
17
  - Use `litopencode doctor --root <workspace>` to inspect package metadata, config source, runtime paths, and ledger location without writing files.
18
18
  - Use `litopencode install --dry-run --root <workspace>` to preview the `opencode.json` plugin mutation without writing files.
19
19
 
@@ -41,7 +41,7 @@ After the source gates pass, verify the packed artifact in a temporary directory
41
41
  ```sh
42
42
  tmp="$(mktemp -d)"
43
43
  npm pack --pack-destination "$tmp"
44
- tar -xzf "$tmp"/litopencode-0.0.3.tgz -C "$tmp"
44
+ tar -xzf "$tmp"/litopencode-0.0.4.tgz -C "$tmp"
45
45
  node --input-type=module -e "import('$tmp/package/dist/index.js').then((m) => console.log(m.default?.id ?? m.pluginId))"
46
46
  (
47
47
  cd "$tmp/package"
@@ -73,7 +73,7 @@ mkdir "$tmp/consumer"
73
73
  (
74
74
  cd "$tmp/consumer"
75
75
  npm init -y
76
- npm install "$tmp"/litopencode-0.0.3.tgz
76
+ npm install "$tmp"/litopencode-0.0.4.tgz
77
77
  npm ls litopencode --all
78
78
  node --input-type=module -e "import('litopencode').then((m) => console.log(m.default.id))"
79
79
  node_modules/.bin/litopencode --help
@@ -89,7 +89,7 @@ Expected results:
89
89
  - `@opencode-ai/plugin` is declared as an optional peer for TypeScript host typings without forcing npx/global installs to pull its transitive runtime tree
90
90
  - package import prints `litopencode`
91
91
  - CLI help, doctor, install dry-run, and write-enabled install exit 0
92
- - write-enabled install creates or updates `opencode.json` without echoing unrelated config secrets
92
+ - write-enabled custom-root install creates or updates `opencode.json` without echoing unrelated config secrets
93
93
  - temporary project cleanup removes the probe directory
94
94
 
95
95
  ## OpenCode Host Probe
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litopencode",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "LitOpenCode OpenCode plugin bootstrap package.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,8 +13,8 @@
13
13
  "import": "./dist/index.js"
14
14
  },
15
15
  "./server": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js"
16
+ "types": "./dist/server.d.ts",
17
+ "import": "./dist/server.js"
18
18
  }
19
19
  },
20
20
  "scripts": {