@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.
- package/dist/cli.js +406 -12
- package/dist/cli.js.map +1 -1
- package/dist/config.test.js +9 -9
- package/dist/config.test.js.map +1 -1
- package/dist/detect.test.js +4 -3
- package/dist/detect.test.js.map +1 -1
- package/dist/docker.d.ts +7 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +17 -0
- package/dist/docker.js.map +1 -0
- package/dist/docker.test.d.ts +2 -0
- package/dist/docker.test.d.ts.map +1 -0
- package/dist/docker.test.js +12 -0
- package/dist/docker.test.js.map +1 -0
- package/dist/health.d.ts +4 -0
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +40 -1
- package/dist/health.js.map +1 -1
- package/dist/health.test.js +21 -2
- package/dist/health.test.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/installer.test.js +2 -2
- package/dist/installer.test.js.map +1 -1
- package/dist/lifecycle.d.ts +6 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +26 -11
- package/dist/lifecycle.js.map +1 -1
- package/dist/lifecycle.test.js +5 -4
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/manifest.js +4 -4
- package/dist/manifest.js.map +1 -1
- package/dist/skills-baseline.d.ts +7 -0
- package/dist/skills-baseline.d.ts.map +1 -0
- package/dist/skills-baseline.js +9 -0
- package/dist/skills-baseline.js.map +1 -0
- package/dist/skills.d.ts +110 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +429 -0
- package/dist/skills.js.map +1 -0
- package/dist/skills.test.d.ts +2 -0
- package/dist/skills.test.d.ts.map +1 -0
- package/dist/skills.test.js +416 -0
- package/dist/skills.test.js.map +1 -0
- package/dist/tenant.d.ts +13 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +105 -0
- package/dist/tenant.js.map +1 -0
- package/dist/tenant.test.d.ts +2 -0
- package/dist/tenant.test.d.ts.map +1 -0
- package/dist/tenant.test.js +37 -0
- package/dist/tenant.test.js.map +1 -0
- package/package.json +15 -5
- package/src/cli.ts +457 -12
- package/src/config.test.ts +9 -9
- package/src/detect.test.ts +4 -3
- package/src/docker.test.ts +12 -0
- package/src/docker.ts +23 -0
- package/src/health.test.ts +23 -1
- package/src/health.ts +45 -1
- package/src/index.ts +37 -3
- package/src/installer.test.ts +2 -2
- package/src/lifecycle.test.ts +6 -5
- package/src/lifecycle.ts +29 -10
- package/src/manifest.ts +4 -4
- package/src/skills-baseline.ts +14 -0
- package/src/skills.test.ts +503 -0
- package/src/skills.ts +512 -0
- package/src/tenant.test.ts +49 -0
- package/src/tenant.ts +120 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@otto-assistant/otto",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Otto — terminal UI distribution wrapper for opencode + kimaki +
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Otto — terminal UI distribution wrapper for opencode + kimaki + mempalace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"otto": "./dist/cli.js"
|
|
@@ -15,8 +15,17 @@
|
|
|
15
15
|
"default": "./dist/index.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
-
"files": [
|
|
19
|
-
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"otto",
|
|
24
|
+
"opencode",
|
|
25
|
+
"kimaki",
|
|
26
|
+
"ai-agent",
|
|
27
|
+
"distribution"
|
|
28
|
+
],
|
|
20
29
|
"license": "MIT",
|
|
21
30
|
"repository": {
|
|
22
31
|
"type": "git",
|
|
@@ -25,6 +34,7 @@
|
|
|
25
34
|
"scripts": {
|
|
26
35
|
"build": "rm -rf dist *.tsbuildinfo && tsc && chmod +x dist/cli.js",
|
|
27
36
|
"dev": "tsc --watch",
|
|
37
|
+
"build:artifacts": "./scripts/build-local-artifacts.sh",
|
|
28
38
|
"prepublishOnly": "pnpm build",
|
|
29
39
|
"test": "vitest run",
|
|
30
40
|
"test:watch": "vitest"
|
|
@@ -34,7 +44,7 @@
|
|
|
34
44
|
"picocolors": "^1.1.1"
|
|
35
45
|
},
|
|
36
46
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^22.
|
|
47
|
+
"@types/node": "^22.19.17",
|
|
38
48
|
"typescript": "^5.7.3",
|
|
39
49
|
"vitest": "^3.2.0"
|
|
40
50
|
}
|
package/src/cli.ts
CHANGED
|
@@ -15,9 +15,28 @@ import {
|
|
|
15
15
|
type OpenCodeConfig,
|
|
16
16
|
} from "./config.js"
|
|
17
17
|
import { installMissingPackages, upgradePackage, planStableUpgrades } from "./installer.js"
|
|
18
|
+
import fs from "node:fs"
|
|
19
|
+
import path from "node:path"
|
|
18
20
|
import { hasKimakiBinary, restartKimaki } from "./lifecycle.js"
|
|
19
|
-
import { checkPackagePresence, checkConfigHealth, checkDirectoryHealth } from "./health.js"
|
|
21
|
+
import { checkPackagePresence, checkConfigHealth, checkDirectoryHealth, checkTenantHealth } from "./health.js"
|
|
20
22
|
import { syncUpstreams } from "./sync.js"
|
|
23
|
+
import { runCompose } from "./docker.js"
|
|
24
|
+
import { ensureTenantScaffold, resolveTenantMode } from "./tenant.js"
|
|
25
|
+
import {
|
|
26
|
+
searchSkills,
|
|
27
|
+
getAllIndexedSkills,
|
|
28
|
+
listInstalledSkills,
|
|
29
|
+
installSkillFromIndex,
|
|
30
|
+
installSkillsBaseline,
|
|
31
|
+
removeSkill,
|
|
32
|
+
ensureSkillsIndex,
|
|
33
|
+
getConfiguredRepos,
|
|
34
|
+
loadSkillsIndex,
|
|
35
|
+
DEFAULT_SKILL_REPOS,
|
|
36
|
+
type SkillIndexEntry,
|
|
37
|
+
type SkillMeta,
|
|
38
|
+
} from "./skills.js"
|
|
39
|
+
import { GENTLEMAN_SKILLS_BASELINE } from "./skills-baseline.js"
|
|
21
40
|
|
|
22
41
|
const args = process.argv.slice(2)
|
|
23
42
|
const command = args[0] ?? ""
|
|
@@ -82,7 +101,23 @@ async function cmdInstall(): Promise<void> {
|
|
|
82
101
|
console.log("Created otto-subagent-threads skill")
|
|
83
102
|
}
|
|
84
103
|
|
|
85
|
-
// 5.
|
|
104
|
+
// 5. Install Otto-core skills from skills repo (best-effort)
|
|
105
|
+
try {
|
|
106
|
+
ensureSkillsIndex()
|
|
107
|
+
const allSkills = getAllIndexedSkills()
|
|
108
|
+
const installedSkillNames = new Set(listInstalledSkills().map((s) => s.name))
|
|
109
|
+
const coreSkills = allSkills.filter(
|
|
110
|
+
(s) => s.source === "otto-assistant/skills" && !installedSkillNames.has(s.name),
|
|
111
|
+
)
|
|
112
|
+
for (const skill of coreSkills) {
|
|
113
|
+
const ok = installSkillFromIndex(skill.name)
|
|
114
|
+
if (ok) console.log(`Installed skill: ${skill.name}`)
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
console.log("⚠ Could not fetch skills from GitHub (offline?). Skipping.")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 6. Restart kimaki if needed — but NOT if running inside kimaki
|
|
86
121
|
// (kimaki restart kills the current opencode session)
|
|
87
122
|
const runningInsideKimaki = !!process.env.KIMAKI
|
|
88
123
|
if (configChanged || installed.length > 0) {
|
|
@@ -198,6 +233,18 @@ async function cmdStatus(): Promise<void> {
|
|
|
198
233
|
console.log(` ask before thread delete: ${configHealth.subagentThreadsAskBeforeDelete ? "yes" : "no"}`)
|
|
199
234
|
console.log(` auto delete thread on complete: ${configHealth.subagentThreadsAutoDelete ? "yes" : "no"}`)
|
|
200
235
|
console.log(` kimaki process: ${configHealth.kimakiRunning ? "running" : "not running"}`)
|
|
236
|
+
|
|
237
|
+
console.log("\nSkills:")
|
|
238
|
+
const skillsInstalled = listInstalledSkills()
|
|
239
|
+
console.log(` installed: ${skillsInstalled.length > 0 ? skillsInstalled.map((s) => s.name).join(", ") : "(none)"}`)
|
|
240
|
+
try {
|
|
241
|
+
const index = loadSkillsIndex()
|
|
242
|
+
const repoCount = Object.keys(index.repos).length
|
|
243
|
+
const totalIndexed = getAllIndexedSkills().length
|
|
244
|
+
console.log(` indexed: ${totalIndexed} skills from ${repoCount} repos`)
|
|
245
|
+
} catch {
|
|
246
|
+
console.log(" indexed: (unavailable)")
|
|
247
|
+
}
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
async function cmdDoctor(): Promise<void> {
|
|
@@ -223,9 +270,9 @@ async function cmdDoctor(): Promise<void> {
|
|
|
223
270
|
hasErrors = true
|
|
224
271
|
}
|
|
225
272
|
if (configHealth.memoryPluginEnabled) {
|
|
226
|
-
console.log(" ✓
|
|
273
|
+
console.log(" ✓ mempalace plugin enabled")
|
|
227
274
|
} else {
|
|
228
|
-
console.log(" ✗
|
|
275
|
+
console.log(" ✗ mempalace plugin NOT enabled — run `otto install`")
|
|
229
276
|
hasErrors = true
|
|
230
277
|
}
|
|
231
278
|
if (configHealth.subagentPolicyInjected) {
|
|
@@ -248,9 +295,386 @@ async function cmdDoctor(): Promise<void> {
|
|
|
248
295
|
if (d.status === "error") hasErrors = true
|
|
249
296
|
}
|
|
250
297
|
|
|
298
|
+
console.log("\nChecking skills...")
|
|
299
|
+
const skillsInstalled = listInstalledSkills()
|
|
300
|
+
if (skillsInstalled.length > 0) {
|
|
301
|
+
console.log(` ✓ ${skillsInstalled.length} skill(s) installed`)
|
|
302
|
+
} else {
|
|
303
|
+
console.log(" ⚠ No skills installed — run `otto skills add --all`")
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const totalIndexed = getAllIndexedSkills().length
|
|
307
|
+
if (totalIndexed > 0) {
|
|
308
|
+
console.log(` ✓ Skills index available (${totalIndexed} skills)`)
|
|
309
|
+
} else {
|
|
310
|
+
console.log(" ⚠ Skills index empty — run `otto skills update`")
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
console.log(" ⚠ Skills index unavailable")
|
|
314
|
+
}
|
|
315
|
+
|
|
251
316
|
console.log(hasErrors ? "\n✗ Issues found. Run `otto install` to fix." : "\n✓ All checks passed!")
|
|
252
317
|
}
|
|
253
318
|
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// otto skills sub-commands
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
async function cmdSkills(subArgs: string[]): Promise<void> {
|
|
324
|
+
const skillCommand = subArgs[0] ?? ""
|
|
325
|
+
|
|
326
|
+
switch (skillCommand) {
|
|
327
|
+
case "list":
|
|
328
|
+
cmdSkillsList()
|
|
329
|
+
break
|
|
330
|
+
case "search": {
|
|
331
|
+
const query = subArgs.slice(1).join(" ")
|
|
332
|
+
if (!query) {
|
|
333
|
+
console.log("Usage: otto skills search <query>")
|
|
334
|
+
process.exit(1)
|
|
335
|
+
}
|
|
336
|
+
cmdSkillsSearch(query)
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
case "browse":
|
|
340
|
+
cmdSkillsBrowse()
|
|
341
|
+
break
|
|
342
|
+
case "add": {
|
|
343
|
+
const arg = subArgs[1]
|
|
344
|
+
if (!arg || arg === "--all") {
|
|
345
|
+
await cmdSkillsAddAll()
|
|
346
|
+
} else {
|
|
347
|
+
await cmdSkillsAddOne(arg)
|
|
348
|
+
}
|
|
349
|
+
break
|
|
350
|
+
}
|
|
351
|
+
case "remove": {
|
|
352
|
+
const name = subArgs[1]
|
|
353
|
+
if (!name) {
|
|
354
|
+
console.log("Usage: otto skills remove <name>")
|
|
355
|
+
process.exit(1)
|
|
356
|
+
}
|
|
357
|
+
cmdSkillsRemove(name)
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
case "update":
|
|
361
|
+
cmdSkillsUpdate()
|
|
362
|
+
break
|
|
363
|
+
case "repos":
|
|
364
|
+
cmdSkillsRepos()
|
|
365
|
+
break
|
|
366
|
+
default:
|
|
367
|
+
console.log(`Otto skills — discover and install agent skills from public repos
|
|
368
|
+
|
|
369
|
+
Usage:
|
|
370
|
+
otto skills search <query> Search skills by name/description
|
|
371
|
+
otto skills browse Browse all available skills
|
|
372
|
+
otto skills list List installed skills
|
|
373
|
+
otto skills add <name> Install a skill
|
|
374
|
+
otto skills add --all Install all skills from otto-assistant/skills
|
|
375
|
+
otto skills update Refresh skills index from GitHub
|
|
376
|
+
otto skills remove <name> Remove an installed skill
|
|
377
|
+
otto skills repos Show configured skill repositories
|
|
378
|
+
`)
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function cmdSkillsSearch(query: string): void {
|
|
384
|
+
console.log(`Searching: "${query}"\n`)
|
|
385
|
+
|
|
386
|
+
const { refreshed } = ensureSkillsIndex()
|
|
387
|
+
if (refreshed > 0) {
|
|
388
|
+
console.log(`Updated index (${refreshed} repo(s) refreshed).\n`)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const results = searchSkills(query)
|
|
392
|
+
if (results.length === 0) {
|
|
393
|
+
console.log("No skills found.")
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
for (const skill of results) {
|
|
398
|
+
console.log(` ${skill.name} — ${skill.description}`)
|
|
399
|
+
console.log(` source: ${skill.source}`)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log(`\n${results.length} skill(s) found. Install with: otto skills add <name>`)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function cmdSkillsBrowse(): void {
|
|
406
|
+
console.log("Otto skills — browsing all available\n")
|
|
407
|
+
|
|
408
|
+
const { refreshed } = ensureSkillsIndex()
|
|
409
|
+
if (refreshed > 0) {
|
|
410
|
+
console.log(`Updated index (${refreshed} repo(s) refreshed).\n`)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const allSkills = getAllIndexedSkills()
|
|
414
|
+
const installed = new Set(listInstalledSkills().map((s) => s.name))
|
|
415
|
+
|
|
416
|
+
// Group by source repo
|
|
417
|
+
const byRepo: Record<string, SkillIndexEntry[]> = {}
|
|
418
|
+
for (const skill of allSkills) {
|
|
419
|
+
if (!byRepo[skill.source]) byRepo[skill.source] = []
|
|
420
|
+
byRepo[skill.source].push(skill)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for (const [repo, skills] of Object.entries(byRepo)) {
|
|
424
|
+
console.log(`${repo} (${skills.length} skills):`)
|
|
425
|
+
for (const skill of skills) {
|
|
426
|
+
const icon = installed.has(skill.name) ? "✓" : "•"
|
|
427
|
+
console.log(` ${icon} ${skill.name} — ${skill.description}`)
|
|
428
|
+
}
|
|
429
|
+
console.log()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const totalAvailable = allSkills.filter((s) => !installed.has(s.name)).length
|
|
433
|
+
console.log(`${allSkills.length} total, ${totalAvailable} available to install.`)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function cmdSkillsList(): void {
|
|
437
|
+
console.log("Otto skills\n")
|
|
438
|
+
|
|
439
|
+
const installed = listInstalledSkills()
|
|
440
|
+
if (installed.length > 0) {
|
|
441
|
+
console.log("Installed:")
|
|
442
|
+
for (const s of installed) {
|
|
443
|
+
console.log(` ✓ ${s.name} — ${s.description}`)
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
console.log("Installed: (none)")
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log(`\nUse "otto skills browse" to see all available skills.`)
|
|
450
|
+
console.log(`Use "otto skills search <query>" to search.`)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function cmdSkillsAddOne(name: string): Promise<void> {
|
|
454
|
+
console.log(`Installing skill: ${name}\n`)
|
|
455
|
+
|
|
456
|
+
ensureSkillsIndex()
|
|
457
|
+
|
|
458
|
+
const success = installSkillFromIndex(name)
|
|
459
|
+
if (!success) {
|
|
460
|
+
console.error(`Error: skill "${name}" not found. Run "otto skills search <query>" to find skills.`)
|
|
461
|
+
process.exit(1)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log(`Installed ${name} → ~/.config/opencode/skills/${name}/`)
|
|
465
|
+
console.log("Done!")
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function cmdSkillsAddAll(): Promise<void> {
|
|
469
|
+
console.log("Installing all skills from otto-assistant/skills...\n")
|
|
470
|
+
|
|
471
|
+
ensureSkillsIndex()
|
|
472
|
+
const allSkills = getAllIndexedSkills()
|
|
473
|
+
const ottoSkills = allSkills.filter((s) => s.source === "otto-assistant/skills")
|
|
474
|
+
|
|
475
|
+
if (ottoSkills.length === 0) {
|
|
476
|
+
console.log("No skills found in otto-assistant/skills. Check your connection.")
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const installed = new Set(listInstalledSkills().map((s) => s.name))
|
|
481
|
+
let added = 0
|
|
482
|
+
|
|
483
|
+
for (const skill of ottoSkills) {
|
|
484
|
+
if (installed.has(skill.name)) {
|
|
485
|
+
console.log(` ✓ ${skill.name} (already installed)`)
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
const success = installSkillFromIndex(skill.name)
|
|
489
|
+
if (success) {
|
|
490
|
+
console.log(` + ${skill.name}`)
|
|
491
|
+
added++
|
|
492
|
+
} else {
|
|
493
|
+
console.log(` ✗ ${skill.name} (failed)`)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (added === 0) {
|
|
498
|
+
console.log("\nAll skills already installed.")
|
|
499
|
+
} else {
|
|
500
|
+
console.log(`\nInstalled ${added} skill(s).`)
|
|
501
|
+
}
|
|
502
|
+
console.log("Done!")
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function cmdSkillsUpdate(): void {
|
|
506
|
+
console.log("Refreshing skills index from GitHub...\n")
|
|
507
|
+
|
|
508
|
+
const { refreshed, total } = ensureSkillsIndex(0) // force refresh all
|
|
509
|
+
|
|
510
|
+
if (refreshed === 0 && total === 0) {
|
|
511
|
+
console.log("No repos configured.")
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(`Refreshed ${refreshed}/${total} repo(s).`)
|
|
516
|
+
|
|
517
|
+
const allSkills = getAllIndexedSkills()
|
|
518
|
+
console.log(`Index now has ${allSkills.length} skills.`)
|
|
519
|
+
console.log("Done!")
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function cmdSkillsRemove(name: string): void {
|
|
523
|
+
console.log(`Removing skill: ${name}\n`)
|
|
524
|
+
|
|
525
|
+
const success = removeSkill(name)
|
|
526
|
+
if (!success) {
|
|
527
|
+
console.error(`Error: skill "${name}" is not installed.`)
|
|
528
|
+
process.exit(1)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
console.log(`Removed ${name}.`)
|
|
532
|
+
console.log("Done!")
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function cmdSkillsRepos(): void {
|
|
536
|
+
console.log("Configured skill repositories:\n")
|
|
537
|
+
|
|
538
|
+
const repos = getConfiguredRepos()
|
|
539
|
+
for (const repo of repos) {
|
|
540
|
+
console.log(` ${repo}`)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
console.log(`\n${repos.length} repo(s) configured.`)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function cmdTenant(subArgs: string[]): Promise<void> {
|
|
547
|
+
const tenantCommand = subArgs[0] ?? ""
|
|
548
|
+
const tenantPathArg = subArgs[1]
|
|
549
|
+
|
|
550
|
+
if (tenantCommand === "skills") {
|
|
551
|
+
const action = subArgs[1] ?? ""
|
|
552
|
+
const pathArg = subArgs[2]
|
|
553
|
+
if (action !== "bootstrap" || !pathArg) {
|
|
554
|
+
console.log("Usage: otto tenant skills bootstrap <path>")
|
|
555
|
+
process.exit(1)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const tenantPath = path.resolve(pathArg)
|
|
559
|
+
const skillsDir = path.join(tenantPath, "memory", "opencode", "skills")
|
|
560
|
+
|
|
561
|
+
ensureSkillsIndex()
|
|
562
|
+
const report = installSkillsBaseline(GENTLEMAN_SKILLS_BASELINE, skillsDir)
|
|
563
|
+
|
|
564
|
+
console.log(`Skills baseline bootstrap: ${tenantPath}`)
|
|
565
|
+
console.log(` Installed: ${report.installed.length > 0 ? report.installed.join(", ") : "(none)"}`)
|
|
566
|
+
console.log(` Already present: ${report.alreadyPresent.length > 0 ? report.alreadyPresent.join(", ") : "(none)"}`)
|
|
567
|
+
if (report.failed.length > 0) {
|
|
568
|
+
console.log(` Failed: ${report.failed.join(", ")}`)
|
|
569
|
+
process.exitCode = 2
|
|
570
|
+
} else {
|
|
571
|
+
console.log(" Failed: (none)")
|
|
572
|
+
}
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (!tenantPathArg) {
|
|
577
|
+
console.log("Usage: otto tenant <init|up|down|status|logs> <path>")
|
|
578
|
+
process.exit(1)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const tenantPath = path.resolve(tenantPathArg)
|
|
582
|
+
|
|
583
|
+
switch (tenantCommand) {
|
|
584
|
+
case "init": {
|
|
585
|
+
const result = ensureTenantScaffold(tenantPath)
|
|
586
|
+
if (result.created.length === 0) {
|
|
587
|
+
console.log(`Tenant scaffold already exists: ${tenantPath}`)
|
|
588
|
+
} else {
|
|
589
|
+
console.log(`Tenant scaffold ready: ${tenantPath}`)
|
|
590
|
+
console.log(`Created: ${result.created.join(", ")}`)
|
|
591
|
+
}
|
|
592
|
+
console.log(`
|
|
593
|
+
Next steps — get your tenant running in 3 steps:
|
|
594
|
+
|
|
595
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
596
|
+
Step 1: Create a Discord Bot
|
|
597
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
598
|
+
|
|
599
|
+
1. Open https://discord.com/developers/applications
|
|
600
|
+
2. Click "New Application" → give it a name → Create
|
|
601
|
+
3. Go to "Bot" tab → Click "Reset Token" → Copy the token
|
|
602
|
+
4. Under "Privileged Gateway Intents" enable:
|
|
603
|
+
✅ Message Content Intent
|
|
604
|
+
✅ Server Members Intent (optional)
|
|
605
|
+
5. Go to "OAuth2" tab → "URL Generator"
|
|
606
|
+
Scopes: bot
|
|
607
|
+
Permissions: Send Messages, Read Message History, Add Reactions
|
|
608
|
+
6. Open the generated URL → add bot to your Discord server
|
|
609
|
+
|
|
610
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
611
|
+
Step 2: Configure your tenant
|
|
612
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
613
|
+
|
|
614
|
+
Edit ${tenantPath}/.env with your bot token:
|
|
615
|
+
|
|
616
|
+
KIMAKI_BOT_TOKEN=your-bot-token-here
|
|
617
|
+
|
|
618
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
619
|
+
Step 3: Start your tenant
|
|
620
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
621
|
+
|
|
622
|
+
docker compose -f ${tenantPath}/compose.yml up -d
|
|
623
|
+
|
|
624
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
625
|
+
|
|
626
|
+
Useful commands:
|
|
627
|
+
otto tenant status ${tenantPathArg} — check health
|
|
628
|
+
otto tenant logs ${tenantPathArg} — view logs
|
|
629
|
+
otto tenant down ${tenantPathArg} — stop tenant
|
|
630
|
+
otto tenant skills bootstrap ${tenantPathArg} — install baseline skills
|
|
631
|
+
`)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
case "up": {
|
|
635
|
+
const mode = resolveTenantMode(process.env.OTTO_MODE)
|
|
636
|
+
if (mode === "admin") {
|
|
637
|
+
console.log("⚠ OTTO_MODE=admin enabled: tenant has elevated runtime profile.")
|
|
638
|
+
}
|
|
639
|
+
runCompose(tenantPath, ["up", "-d"])
|
|
640
|
+
return
|
|
641
|
+
}
|
|
642
|
+
case "down":
|
|
643
|
+
runCompose(tenantPath, ["down"])
|
|
644
|
+
return
|
|
645
|
+
case "logs": {
|
|
646
|
+
const follow = subArgs.includes("--follow") ? ["--follow"] : []
|
|
647
|
+
runCompose(tenantPath, ["logs", ...follow])
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
case "status": {
|
|
651
|
+
const health = checkTenantHealth({ tenantPath })
|
|
652
|
+
console.log(`Tenant status: ${tenantPath}`)
|
|
653
|
+
for (const item of health) {
|
|
654
|
+
const icon = item.status === "ok" ? "✓" : item.status === "warn" ? "⚠" : "✗"
|
|
655
|
+
console.log(` ${icon} ${item.name}: ${item.message}`)
|
|
656
|
+
}
|
|
657
|
+
const composeExists = fs.existsSync(path.join(tenantPath, "compose.yml"))
|
|
658
|
+
const skipComposePs = process.env.OTTO_SKIP_COMPOSE_PS === "1"
|
|
659
|
+
if (composeExists && !skipComposePs) {
|
|
660
|
+
try {
|
|
661
|
+
runCompose(tenantPath, ["ps"])
|
|
662
|
+
} catch {
|
|
663
|
+
console.log(" ⚠ docker compose ps failed")
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
default:
|
|
669
|
+
console.log("Usage: otto tenant <init|up|down|status|logs> <path> | otto tenant skills bootstrap <path>")
|
|
670
|
+
process.exit(1)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
// Main router
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
|
|
254
678
|
async function main(): Promise<void> {
|
|
255
679
|
switch (command) {
|
|
256
680
|
case "install":
|
|
@@ -268,17 +692,38 @@ async function main(): Promise<void> {
|
|
|
268
692
|
case "sync":
|
|
269
693
|
await syncUpstreams()
|
|
270
694
|
break
|
|
695
|
+
case "skills":
|
|
696
|
+
await cmdSkills(args.slice(1))
|
|
697
|
+
break
|
|
698
|
+
case "tenant":
|
|
699
|
+
await cmdTenant(args.slice(1))
|
|
700
|
+
break
|
|
271
701
|
default:
|
|
272
|
-
console.log(`Otto — terminal UI distribution for opencode + kimaki +
|
|
702
|
+
console.log(`Otto — terminal UI distribution for opencode + kimaki + mempalace
|
|
273
703
|
|
|
274
704
|
Usage:
|
|
275
|
-
otto
|
|
276
|
-
otto
|
|
277
|
-
otto
|
|
278
|
-
otto
|
|
279
|
-
otto
|
|
280
|
-
otto
|
|
281
|
-
|
|
705
|
+
otto tenant init <path> Create compose-first tenant scaffold
|
|
706
|
+
otto tenant up <path> Start tenant with docker compose
|
|
707
|
+
otto tenant down <path> Stop tenant with docker compose
|
|
708
|
+
otto tenant status <path> Show tenant preflight + compose status
|
|
709
|
+
otto tenant logs <path> Show tenant logs (add --follow)
|
|
710
|
+
otto tenant skills bootstrap <path> Install baseline skills for tenant
|
|
711
|
+
|
|
712
|
+
otto install Legacy: install missing npm packages + configure
|
|
713
|
+
otto upgrade Legacy: upgrade to stable (manifest-pinned) versions
|
|
714
|
+
otto upgrade stable Legacy: upgrade to manifest-pinned versions
|
|
715
|
+
otto upgrade latest Legacy: upgrade to npm latest versions
|
|
716
|
+
otto status Show installed versions + config health
|
|
717
|
+
otto doctor Validate all integration points
|
|
718
|
+
otto sync Trigger upstream sync for all forked repos
|
|
719
|
+
otto skills search <q> Search skills across public repos
|
|
720
|
+
otto skills browse Browse all available skills
|
|
721
|
+
otto skills list List installed skills
|
|
722
|
+
otto skills add <name> Install a skill
|
|
723
|
+
otto skills add --all Install all skills from otto-assistant/skills
|
|
724
|
+
otto skills update Refresh skills index
|
|
725
|
+
otto skills remove <name> Remove an installed skill
|
|
726
|
+
otto skills repos Show configured skill repositories
|
|
282
727
|
`)
|
|
283
728
|
break
|
|
284
729
|
}
|
package/src/config.test.ts
CHANGED
|
@@ -18,20 +18,20 @@ import os from "node:os"
|
|
|
18
18
|
describe("config", () => {
|
|
19
19
|
it("adds plugin to empty config", () => {
|
|
20
20
|
const config: OpenCodeConfig = {}
|
|
21
|
-
const result = mergePlugins(config, "
|
|
22
|
-
expect(result.plugin).toEqual(["
|
|
21
|
+
const result = mergePlugins(config, "mempalace")
|
|
22
|
+
expect(result.plugin).toEqual(["mempalace"])
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
it("appends plugin to existing array", () => {
|
|
26
26
|
const config: OpenCodeConfig = { plugin: ["existing-plugin"] }
|
|
27
|
-
const result = mergePlugins(config, "
|
|
28
|
-
expect(result.plugin).toEqual(["existing-plugin", "
|
|
27
|
+
const result = mergePlugins(config, "mempalace")
|
|
28
|
+
expect(result.plugin).toEqual(["existing-plugin", "mempalace"])
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
it("does not duplicate existing plugin", () => {
|
|
32
|
-
const config: OpenCodeConfig = { plugin: ["
|
|
33
|
-
const result = mergePlugins(config, "
|
|
34
|
-
expect(result.plugin).toEqual(["
|
|
32
|
+
const config: OpenCodeConfig = { plugin: ["mempalace"] }
|
|
33
|
+
const result = mergePlugins(config, "mempalace")
|
|
34
|
+
expect(result.plugin).toEqual(["mempalace"])
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
it("preserves other config fields", () => {
|
|
@@ -40,10 +40,10 @@ describe("config", () => {
|
|
|
40
40
|
plugin: ["existing"],
|
|
41
41
|
provider: { cursor: { name: "Cursor" } },
|
|
42
42
|
}
|
|
43
|
-
const result = mergePlugins(config, "
|
|
43
|
+
const result = mergePlugins(config, "mempalace")
|
|
44
44
|
expect(result.model).toBe("gpt-4")
|
|
45
45
|
expect(result.provider).toEqual({ cursor: { name: "Cursor" } })
|
|
46
|
-
expect(result.plugin).toEqual(["existing", "
|
|
46
|
+
expect(result.plugin).toEqual(["existing", "mempalace"])
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
it("readOttoConfig returns defaults when otto.json missing", () => {
|
package/src/detect.test.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { describe, expect, it } from "vitest"
|
|
|
2
2
|
import { getInstalledVersion, detectPackage, type InstalledPackage } from "./detect.js"
|
|
3
3
|
|
|
4
4
|
describe("detect", () => {
|
|
5
|
-
it("returns null for non-existent package", () => {
|
|
5
|
+
it("returns null for non-existent package", { timeout: 30_000 }, () => {
|
|
6
6
|
const result = getInstalledVersion("nonexistent-package-xyz-123")
|
|
7
7
|
expect(result).toBeNull()
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
// These tests require kimaki to be globally installed
|
|
11
|
-
const
|
|
10
|
+
// These tests require kimaki to be globally installed
|
|
11
|
+
const hasKimakiInstalled = getInstalledVersion("kimaki") !== null
|
|
12
|
+
const describeIfInstalled = hasKimakiInstalled ? describe : describe.skip
|
|
12
13
|
|
|
13
14
|
describeIfInstalled("with kimaki installed", () => {
|
|
14
15
|
it("detects an existing global package", () => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { buildComposeCommand } from "./docker.js"
|
|
3
|
+
|
|
4
|
+
describe("docker", () => {
|
|
5
|
+
it("builds docker compose command with tenant path", () => {
|
|
6
|
+
const cmd = buildComposeCommand("/tmp/tenant-a", ["up", "-d"])
|
|
7
|
+
expect(cmd).toEqual({
|
|
8
|
+
command: "docker",
|
|
9
|
+
args: ["compose", "-f", "/tmp/tenant-a/compose.yml", "up", "-d"],
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
})
|
package/src/docker.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export interface DockerComposeCommand {
|
|
5
|
+
command: string
|
|
6
|
+
args: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildComposeCommand(tenantPath: string, subArgs: string[]): DockerComposeCommand {
|
|
10
|
+
const composeFile = path.join(path.resolve(tenantPath), "compose.yml")
|
|
11
|
+
return {
|
|
12
|
+
command: "docker",
|
|
13
|
+
args: ["compose", "-f", composeFile, ...subArgs],
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function runCompose(tenantPath: string, subArgs: string[]): void {
|
|
18
|
+
const built = buildComposeCommand(tenantPath, subArgs)
|
|
19
|
+
execSync(`${built.command} ${built.args.join(" ")}`, {
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
timeout: 120_000,
|
|
22
|
+
})
|
|
23
|
+
}
|