@otto-assistant/otto 0.1.0 → 0.1.1

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 (72) hide show
  1. package/dist/cli.js +406 -12
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.test.js +9 -9
  4. package/dist/config.test.js.map +1 -1
  5. package/dist/detect.test.js +4 -3
  6. package/dist/detect.test.js.map +1 -1
  7. package/dist/docker.d.ts +7 -0
  8. package/dist/docker.d.ts.map +1 -0
  9. package/dist/docker.js +17 -0
  10. package/dist/docker.js.map +1 -0
  11. package/dist/docker.test.d.ts +2 -0
  12. package/dist/docker.test.d.ts.map +1 -0
  13. package/dist/docker.test.js +12 -0
  14. package/dist/docker.test.js.map +1 -0
  15. package/dist/health.d.ts +4 -0
  16. package/dist/health.d.ts.map +1 -1
  17. package/dist/health.js +40 -1
  18. package/dist/health.js.map +1 -1
  19. package/dist/health.test.js +21 -2
  20. package/dist/health.test.js.map +1 -1
  21. package/dist/index.d.ts +11 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/installer.test.js +2 -2
  26. package/dist/installer.test.js.map +1 -1
  27. package/dist/lifecycle.d.ts +6 -0
  28. package/dist/lifecycle.d.ts.map +1 -1
  29. package/dist/lifecycle.js +26 -11
  30. package/dist/lifecycle.js.map +1 -1
  31. package/dist/lifecycle.test.js +5 -4
  32. package/dist/lifecycle.test.js.map +1 -1
  33. package/dist/manifest.js +4 -4
  34. package/dist/manifest.js.map +1 -1
  35. package/dist/skills-baseline.d.ts +7 -0
  36. package/dist/skills-baseline.d.ts.map +1 -0
  37. package/dist/skills-baseline.js +9 -0
  38. package/dist/skills-baseline.js.map +1 -0
  39. package/dist/skills.d.ts +110 -0
  40. package/dist/skills.d.ts.map +1 -0
  41. package/dist/skills.js +429 -0
  42. package/dist/skills.js.map +1 -0
  43. package/dist/skills.test.d.ts +2 -0
  44. package/dist/skills.test.d.ts.map +1 -0
  45. package/dist/skills.test.js +416 -0
  46. package/dist/skills.test.js.map +1 -0
  47. package/dist/tenant.d.ts +13 -0
  48. package/dist/tenant.d.ts.map +1 -0
  49. package/dist/tenant.js +105 -0
  50. package/dist/tenant.js.map +1 -0
  51. package/dist/tenant.test.d.ts +2 -0
  52. package/dist/tenant.test.d.ts.map +1 -0
  53. package/dist/tenant.test.js +37 -0
  54. package/dist/tenant.test.js.map +1 -0
  55. package/package.json +15 -5
  56. package/src/cli.ts +457 -12
  57. package/src/config.test.ts +9 -9
  58. package/src/detect.test.ts +4 -3
  59. package/src/docker.test.ts +12 -0
  60. package/src/docker.ts +23 -0
  61. package/src/health.test.ts +23 -1
  62. package/src/health.ts +45 -1
  63. package/src/index.ts +37 -3
  64. package/src/installer.test.ts +2 -2
  65. package/src/lifecycle.test.ts +6 -5
  66. package/src/lifecycle.ts +29 -10
  67. package/src/manifest.ts +4 -4
  68. package/src/skills-baseline.ts +14 -0
  69. package/src/skills.test.ts +503 -0
  70. package/src/skills.ts +512 -0
  71. package/src/tenant.test.ts +49 -0
  72. package/src/tenant.ts +120 -0
@@ -1,14 +1,18 @@
1
+ import fs from "node:fs"
2
+ import os from "node:os"
3
+ import path from "node:path"
1
4
  import { describe, expect, it } from "vitest"
2
5
  import {
3
6
  checkPackagePresence,
4
7
  checkConfigHealth,
5
8
  checkDirectoryHealth,
9
+ checkTenantHealth,
6
10
  } from "./health.js"
7
11
 
8
12
  describe("health", () => {
9
13
  it("checkPackagePresence returns results for all manifest packages", { timeout: 30_000 }, () => {
10
14
  const results = checkPackagePresence()
11
- expect(results).toHaveLength(2) // opencode-ai + kimaki (not opencode-agent-memory — it's a plugin)
15
+ expect(results).toHaveLength(2) // opencode-ai + kimaki (not mempalace — it's a plugin)
12
16
  for (const r of results) {
13
17
  expect(r).toHaveProperty("name")
14
18
  expect(r).toHaveProperty("installed")
@@ -36,4 +40,22 @@ describe("health", () => {
36
40
  expect(Array.isArray(results)).toBe(true)
37
41
  expect(results.length).toBeGreaterThan(0)
38
42
  })
43
+
44
+ it("checkTenantHealth reports missing memory bind root as error", () => {
45
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "otto-health-"))
46
+ const health = checkTenantHealth({ tenantPath: tmpDir })
47
+ const memoryItem = health.find((h) => h.name === "memory root")
48
+ expect(memoryItem).toBeDefined()
49
+ expect(memoryItem!.status).toBe("error")
50
+ })
51
+
52
+ it("checkTenantHealth reports ok when all paths present", () => {
53
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "otto-health-"))
54
+ fs.mkdirSync(path.join(tmpDir, "memory"))
55
+ fs.mkdirSync(path.join(tmpDir, "projects"))
56
+ fs.writeFileSync(path.join(tmpDir, "compose.yml"), "services:", "utf-8")
57
+ const health = checkTenantHealth({ tenantPath: tmpDir })
58
+ const errors = health.filter((h) => h.status === "error")
59
+ expect(errors).toHaveLength(0)
60
+ })
39
61
  })
package/src/health.ts CHANGED
@@ -69,7 +69,7 @@ export function checkConfigHealth(): ConfigHealth {
69
69
 
70
70
  const config = state.status === "ok" ? state.config : {}
71
71
  const configuredPlugins = config.plugin ?? []
72
- const memoryPluginEnabled = configuredPlugins.includes("opencode-agent-memory")
72
+ const memoryPluginEnabled = configuredPlugins.includes("mempalace")
73
73
 
74
74
  const subagentPolicyInjected = subagentPolicyInjectedInPrompts(config)
75
75
 
@@ -112,3 +112,47 @@ export function checkDirectoryHealth(): HealthResult[] {
112
112
  }
113
113
  return results
114
114
  }
115
+
116
+ export interface TenantHealthInput {
117
+ tenantPath: string
118
+ }
119
+
120
+ export function checkTenantHealth(input: TenantHealthInput): HealthResult[] {
121
+ const results: HealthResult[] = []
122
+ const { tenantPath } = input
123
+
124
+ if (fs.existsSync(tenantPath)) {
125
+ results.push({ name: "tenant path", status: "ok", message: `exists: ${tenantPath}` })
126
+ } else {
127
+ results.push({ name: "tenant path", status: "error", message: `missing: ${tenantPath}` })
128
+ return results
129
+ }
130
+
131
+ const composePath = path.join(tenantPath, "compose.yml")
132
+ if (fs.existsSync(composePath)) {
133
+ results.push({ name: "compose.yml", status: "ok", message: "present" })
134
+ } else {
135
+ results.push({ name: "compose.yml", status: "error", message: "missing — run `otto tenant init`" })
136
+ }
137
+
138
+ const memoryPath = path.join(tenantPath, "memory")
139
+ if (fs.existsSync(memoryPath)) {
140
+ try {
141
+ fs.accessSync(memoryPath, fs.constants.W_OK)
142
+ results.push({ name: "memory root", status: "ok", message: "writable" })
143
+ } catch {
144
+ results.push({ name: "memory root", status: "error", message: "not writable" })
145
+ }
146
+ } else {
147
+ results.push({ name: "memory root", status: "error", message: "missing — run `otto tenant init`" })
148
+ }
149
+
150
+ const projectsPath = path.join(tenantPath, "projects")
151
+ if (fs.existsSync(projectsPath)) {
152
+ results.push({ name: "projects mount", status: "ok", message: "present" })
153
+ } else {
154
+ results.push({ name: "projects mount", status: "warn", message: "missing — will be created on first up" })
155
+ }
156
+
157
+ return results
158
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,40 @@ export {
23
23
  export type { OpenCodeConfig, OttoConfig, OttoSubagentThreads, OttoConfigReadStatus, OpenCodeConfigReadStatus } from "./config.js"
24
24
  export { installPackage, upgradePackage, installMissingPackages, planStableUpgrades } from "./installer.js"
25
25
  export type { UpgradeMode } from "./installer.js"
26
- export { hasKimakiBinary, isKimakiRunning, restartKimaki } from "./lifecycle.js"
27
- export { checkPackagePresence, checkConfigHealth, checkDirectoryHealth } from "./health.js"
28
- export type { HealthResult, PackageCheck, ConfigHealth } from "./health.js"
26
+ export { hasKimakiBinary, isKimakiRunning, restartKimaki, detectBinary } from "./lifecycle.js"
27
+ export { checkPackagePresence, checkConfigHealth, checkDirectoryHealth, checkTenantHealth } from "./health.js"
28
+ export type { HealthResult, PackageCheck, ConfigHealth, TenantHealthInput } from "./health.js"
29
+ export {
30
+ OPENCODE_SKILLS_DIR,
31
+ SKILLS_INDEX_PATH,
32
+ DEFAULT_SKILL_REPOS,
33
+ parseSkillMd,
34
+ loadSkillsIndex,
35
+ saveSkillsIndex,
36
+ isIndexStale,
37
+ ensureSkillsIndex,
38
+ searchSkills,
39
+ getAllIndexedSkills,
40
+ fetchRepoSkillsIndex,
41
+ ghApi,
42
+ fetchRepoDir,
43
+ fetchRepoFile,
44
+ listInstalledSkills,
45
+ installSkillFromIndex,
46
+ installSkillsBaseline,
47
+ removeSkill,
48
+ getConfiguredRepos,
49
+ } from "./skills.js"
50
+ export type { SkillMeta, SkillIndexEntry, SkillsIndex, RepoSyncResult } from "./skills.js"
51
+ export { GENTLEMAN_SKILLS_BASELINE } from "./skills-baseline.js"
52
+ export type { SkillsBootstrapReport } from "./skills-baseline.js"
53
+ export {
54
+ deriveComposeProjectName,
55
+ resolveTenantImage,
56
+ resolveTenantMode,
57
+ ensureTenantMemoryLayout,
58
+ ensureTenantScaffold,
59
+ } from "./tenant.js"
60
+ export type { TenantMode, TenantInitResult } from "./tenant.js"
61
+ export { buildComposeCommand, runCompose } from "./docker.js"
62
+ export type { DockerComposeCommand } from "./docker.js"
@@ -17,8 +17,8 @@ describe("installer", () => {
17
17
 
18
18
  expect(result).toContain("opencode-ai")
19
19
  expect(result).not.toContain("kimaki")
20
- // opencode-agent-memory is a plugin, not a global npm package
21
- expect(result).not.toContain("opencode-agent-memory")
20
+ // mempalace is a plugin, not a global npm package
21
+ expect(result).not.toContain("mempalace")
22
22
  })
23
23
 
24
24
  it("planStableUpgrades returns empty when all packages match pinned", () => {
@@ -1,7 +1,12 @@
1
1
  import { describe, expect, it } from "vitest"
2
- import { hasKimakiBinary, isKimakiRunning, restartKimaki } from "./lifecycle.js"
2
+ import { hasKimakiBinary, isKimakiRunning, restartKimaki, detectBinary } from "./lifecycle.js"
3
3
 
4
4
  describe("lifecycle", () => {
5
+ it("detectBinary returns string or null", () => {
6
+ const result = detectBinary()
7
+ expect(result === null || typeof result === "string").toBe(true)
8
+ })
9
+
5
10
  it("hasKimakiBinary returns boolean", () => {
6
11
  const result = hasKimakiBinary()
7
12
  expect(typeof result).toBe("boolean")
@@ -15,8 +20,4 @@ describe("lifecycle", () => {
15
20
  it("restartKimaki is a function", () => {
16
21
  expect(typeof restartKimaki).toBe("function")
17
22
  })
18
-
19
- it("restartKimaki throws descriptive error when kimaki not found", () => {
20
- expect(typeof restartKimaki).toBe("function")
21
- })
22
23
  })
package/src/lifecycle.ts CHANGED
@@ -1,17 +1,33 @@
1
1
  import { execSync } from "node:child_process"
2
2
 
3
- export function hasKimakiBinary(): boolean {
4
- try {
5
- execSync("which kimaki", { encoding: "utf-8", stdio: "pipe" })
6
- return true
7
- } catch {
8
- return false
3
+ /** Ordered list of binary names to try — bridge is the otto fork, kimaki is upstream. */
4
+ const BIN_NAMES = ["bridge", "kimaki"] as const
5
+
6
+ /**
7
+ * Detect the first available binary name from BIN_NAMES.
8
+ * Returns the binary name ("bridge" | "kimaki") or null if none found.
9
+ */
10
+ export function detectBinary(): string | null {
11
+ for (const name of BIN_NAMES) {
12
+ try {
13
+ execSync(`which ${name}`, { encoding: "utf-8", stdio: "pipe" })
14
+ return name
15
+ } catch {
16
+ continue
17
+ }
9
18
  }
19
+ return null
20
+ }
21
+
22
+ /** @deprecated Use detectBinary() instead. Returns true if any known binary exists. */
23
+ export function hasKimakiBinary(): boolean {
24
+ return detectBinary() !== null
10
25
  }
11
26
 
12
27
  export function isKimakiRunning(): boolean {
28
+ // Check for both process names — "bridge" (otto fork) and "kimaki" (upstream)
13
29
  try {
14
- const output = execSync("pgrep -f kimaki", { encoding: "utf-8", stdio: "pipe" })
30
+ const output = execSync("pgrep -f 'kimaki|bridge'", { encoding: "utf-8", stdio: "pipe" })
15
31
  return output.trim().length > 0
16
32
  } catch {
17
33
  return false
@@ -19,10 +35,13 @@ export function isKimakiRunning(): boolean {
19
35
  }
20
36
 
21
37
  export function restartKimaki(): void {
22
- if (!hasKimakiBinary()) {
23
- throw new Error("kimaki is not installed. Install it first with: npm install -g kimaki")
38
+ const bin = detectBinary()
39
+ if (!bin) {
40
+ throw new Error(
41
+ "Neither 'bridge' nor 'kimaki' binary found. Install with: npm install -g @otto-assistant/bridge",
42
+ )
24
43
  }
25
- execSync("kimaki restart", {
44
+ execSync(`${bin} restart`, {
26
45
  encoding: "utf-8",
27
46
  stdio: "pipe",
28
47
  timeout: 30_000,
package/src/manifest.ts CHANGED
@@ -9,17 +9,17 @@ export interface Manifest {
9
9
  }
10
10
 
11
11
  export const MANIFEST: Manifest = {
12
- version: "0.1.0",
12
+ version: "0.1.1",
13
13
  packages: {
14
14
  "opencode-ai": ">=1.0.115",
15
- "@otto-assistant/bridge": ">=0.4.90",
15
+ "@otto-assistant/bridge": ">=0.6.0",
16
16
  },
17
17
  pinned: {
18
18
  "opencode-ai": "1.2.20",
19
- "@otto-assistant/bridge": "0.4.90",
19
+ "@otto-assistant/bridge": "0.6.2",
20
20
  },
21
21
  plugins: [
22
- "opencode-agent-memory",
22
+ "mempalace",
23
23
  ],
24
24
  }
25
25
 
@@ -0,0 +1,14 @@
1
+ export const GENTLEMAN_SKILLS_BASELINE: string[] = [
2
+ "critique",
3
+ "security-review",
4
+ "simplify",
5
+ "opensrc",
6
+ "playwriter",
7
+ "tuistory",
8
+ ]
9
+
10
+ export interface SkillsBootstrapReport {
11
+ installed: string[]
12
+ alreadyPresent: string[]
13
+ failed: string[]
14
+ }