@hybridlabor-api/aos 4.8.0 → 4.9.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/.agents/agents.md +1 -1
- package/.agents/nodes.json +1 -1
- package/.agents/vendor-manifest.json +23 -1
- package/.claude/agents/godmode-media-eventtech.md +1 -1
- package/.claude/hooks/conventional-commits.mjs +125 -0
- package/.claude/hooks/env-file-protection.mjs +105 -0
- package/.claude/hooks/go-gate.mjs +101 -81
- package/.claude/settings.json +13 -0
- package/.opencode/agents/godmode-media-eventtech.md +1 -1
- package/.opencode/plugins/bdb-aos.js +31 -4
- package/README.de.md +1 -1
- package/README.md +1 -1
- package/README.pt.md +1 -1
- package/THIRD_PARTY_NOTICES.md +88 -0
- package/docs/skills_table.md +1 -1
- package/installer.js +174 -46
- package/package.json +6 -2
- package/packages/aos-cli/README.md +80 -0
- package/packages/aos-cli/bin/aos-cli.mjs +134 -0
- package/packages/aos-cli/core-skills.json +12 -0
- package/packages/aos-cli/extensions/aos.ts +321 -0
- package/packages/aos-cli/package-lock.json +1923 -0
- package/packages/aos-cli/package.json +29 -0
- package/packages/aos-cli/scripts/check-theme.mjs +63 -0
- package/packages/aos-cli/themes/aos.json +97 -0
- package/scripts/build-plugin-manifest.mjs +131 -0
- package/scripts/validate-skills.mjs +81 -6
- package/skills/basic/ao-orchestrator/SKILL.md +116 -0
- package/skills/global_config/agenttrail/SKILL.md +6 -1
- package/skills/global_config/ask-tim/SKILL.md +4 -4
- package/skills/global_config/bash-script-generator/SKILL.md +201 -0
- package/skills/global_config/bash-script-generator/assets/templates/standard-template.sh +96 -0
- package/skills/global_config/bash-script-generator/docs/bash-scripting-guide.md +729 -0
- package/skills/global_config/bash-script-generator/docs/generation-best-practices.md +193 -0
- package/skills/global_config/bash-script-generator/docs/script-patterns.md +566 -0
- package/skills/global_config/bash-script-generator/docs/text-processing-guide.md +437 -0
- package/skills/global_config/bash-script-generator/examples/log-analyzer.sh +92 -0
- package/skills/global_config/bash-script-generator/scripts/generate_script_template.sh +123 -0
- package/skills/global_config/bash-script-generator/scripts/run_ci_checks.sh +172 -0
- package/skills/global_config/bash-script-generator/scripts/test_generator.sh +413 -0
- package/skills/global_config/bash-script-validator/SKILL.md +249 -0
- package/skills/global_config/bash-script-validator/docs/awk-reference.md +449 -0
- package/skills/global_config/bash-script-validator/docs/bash-reference.md +468 -0
- package/skills/global_config/bash-script-validator/docs/common-mistakes.md +623 -0
- package/skills/global_config/bash-script-validator/docs/grep-reference.md +395 -0
- package/skills/global_config/bash-script-validator/docs/regex-reference.md +391 -0
- package/skills/global_config/bash-script-validator/docs/sed-reference.md +454 -0
- package/skills/global_config/bash-script-validator/docs/shell-reference.md +463 -0
- package/skills/global_config/bash-script-validator/docs/shellcheck-reference.md +399 -0
- package/skills/global_config/bash-script-validator/examples/bad-bash.sh +55 -0
- package/skills/global_config/bash-script-validator/examples/bad-shell.sh +54 -0
- package/skills/global_config/bash-script-validator/examples/good-bash.sh +71 -0
- package/skills/global_config/bash-script-validator/examples/good-shell.sh +69 -0
- package/skills/global_config/bash-script-validator/scripts/run_ci_checks.sh +23 -0
- package/skills/global_config/bash-script-validator/scripts/shellcheck_wrapper.sh +174 -0
- package/skills/global_config/bash-script-validator/scripts/test_validate.sh +446 -0
- package/skills/global_config/bash-script-validator/scripts/validate.sh +512 -0
- package/skills/global_config/ci-pipeline/SKILL.md +135 -0
- package/skills/global_config/dispatching-parallel-agents/SKILL.md +170 -0
- package/skills/global_config/dockerfile-generator/SKILL.md +1038 -0
- package/skills/global_config/dockerfile-generator/examples/example.dockerignore +95 -0
- package/skills/global_config/dockerfile-generator/examples/golang-distroless.Dockerfile +34 -0
- package/skills/global_config/dockerfile-generator/examples/java-springboot.Dockerfile +45 -0
- package/skills/global_config/dockerfile-generator/examples/nextjs-production.Dockerfile +49 -0
- package/skills/global_config/dockerfile-generator/examples/nodejs-multistage.Dockerfile +55 -0
- package/skills/global_config/dockerfile-generator/examples/python-fastapi.Dockerfile +48 -0
- package/skills/global_config/dockerfile-generator/references/language_specific_guides.md +510 -0
- package/skills/global_config/dockerfile-generator/references/multistage_builds.md +570 -0
- package/skills/global_config/dockerfile-generator/references/optimization_patterns.md +492 -0
- package/skills/global_config/dockerfile-generator/references/security_best_practices.md +375 -0
- package/skills/global_config/dockerfile-generator/scripts/generate_dockerignore.sh +199 -0
- package/skills/global_config/dockerfile-generator/scripts/generate_golang.sh +172 -0
- package/skills/global_config/dockerfile-generator/scripts/generate_java.sh +185 -0
- package/skills/global_config/dockerfile-generator/scripts/generate_nodejs.sh +279 -0
- package/skills/global_config/dockerfile-generator/scripts/generate_python.sh +218 -0
- package/skills/global_config/dockerfile-generator/scripts/test_generator.sh +210 -0
- package/skills/global_config/dockerfile-validator/SKILL.md +300 -0
- package/skills/global_config/dockerfile-validator/examples/.dockerignore.example +34 -0
- package/skills/global_config/dockerfile-validator/examples/bad-example.Dockerfile +37 -0
- package/skills/global_config/dockerfile-validator/examples/golang-distroless.Dockerfile +50 -0
- package/skills/global_config/dockerfile-validator/examples/good-example.Dockerfile +52 -0
- package/skills/global_config/dockerfile-validator/examples/python-optimized.Dockerfile +53 -0
- package/skills/global_config/dockerfile-validator/examples/security-issues.Dockerfile +42 -0
- package/skills/global_config/dockerfile-validator/references/docker_best_practices.md +348 -0
- package/skills/global_config/dockerfile-validator/references/optimization_guide.md +473 -0
- package/skills/global_config/dockerfile-validator/references/security_checklist.md +208 -0
- package/skills/global_config/dockerfile-validator/scripts/dockerfile-validate.sh +699 -0
- package/skills/global_config/dockerfile-validator/scripts/test_validate.sh +35 -0
- package/skills/global_config/dockerfile-validator/tests/fixtures/copy-before-yarn-lock-read.Dockerfile +6 -0
- package/skills/global_config/dockerfile-validator/tests/fixtures/copy-before-yarn.Dockerfile +6 -0
- package/skills/global_config/dockerfile-validator/tests/fixtures/from-platform-nonroot.Dockerfile +7 -0
- package/skills/global_config/dockerfile-validator/tests/test_regression.sh +164 -0
- package/skills/global_config/finishing-a-development-branch/SKILL.md +228 -0
- package/skills/global_config/github-actions-generator/SKILL.md +353 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/composite/action.yml +82 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/docker/Dockerfile +25 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/docker/action.yml +42 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/docker/entrypoint.sh +27 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/javascript/action.yml +33 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/javascript/index.js +50 -0
- package/skills/global_config/github-actions-generator/assets/templates/action/javascript/package.json +27 -0
- package/skills/global_config/github-actions-generator/assets/templates/workflow/basic_workflow.yml +242 -0
- package/skills/global_config/github-actions-generator/assets/templates/workflow/reusable_workflow.yml +106 -0
- package/skills/global_config/github-actions-generator/examples/README.md +147 -0
- package/skills/global_config/github-actions-generator/examples/actions/setup-node-cached/action.yml +93 -0
- package/skills/global_config/github-actions-generator/examples/caching/docker-buildkit.yml +256 -0
- package/skills/global_config/github-actions-generator/examples/security/dependency-review.yml +62 -0
- package/skills/global_config/github-actions-generator/examples/security/sbom-attestation.yml +119 -0
- package/skills/global_config/github-actions-generator/examples/triggers/chatops-commands.yml +475 -0
- package/skills/global_config/github-actions-generator/examples/triggers/repository-dispatch.yml +418 -0
- package/skills/global_config/github-actions-generator/examples/triggers/workflow-orchestration.yml +404 -0
- package/skills/global_config/github-actions-generator/examples/workflows/docker-build-push.yml +68 -0
- package/skills/global_config/github-actions-generator/examples/workflows/go-ci.yml +161 -0
- package/skills/global_config/github-actions-generator/examples/workflows/monorepo-ci.yml +340 -0
- package/skills/global_config/github-actions-generator/examples/workflows/multi-environment-deploy.yml +406 -0
- package/skills/global_config/github-actions-generator/examples/workflows/nodejs-ci.yml +122 -0
- package/skills/global_config/github-actions-generator/examples/workflows/python-ci.yml +157 -0
- package/skills/global_config/github-actions-generator/examples/workflows/scheduled-tasks.yml +376 -0
- package/skills/global_config/github-actions-generator/references/advanced-triggers.md +917 -0
- package/skills/global_config/github-actions-generator/references/best-practices.md +755 -0
- package/skills/global_config/github-actions-generator/references/common-actions.md +715 -0
- package/skills/global_config/github-actions-generator/references/custom-actions.md +320 -0
- package/skills/global_config/github-actions-generator/references/expressions-and-contexts.md +688 -0
- package/skills/global_config/github-actions-generator/references/modern-features.md +421 -0
- package/skills/global_config/github-actions-generator/scripts/test_generator.sh +344 -0
- package/skills/global_config/github-actions-templates/SKILL.md +7 -0
- package/skills/global_config/github-actions-validator/SKILL.md +576 -0
- package/skills/global_config/github-actions-validator/examples/README.md +88 -0
- package/skills/global_config/github-actions-validator/examples/outdated-versions.yml +76 -0
- package/skills/global_config/github-actions-validator/examples/valid-ci.yml +79 -0
- package/skills/global_config/github-actions-validator/examples/with-errors.yml +47 -0
- package/skills/global_config/github-actions-validator/references/act_usage.md +233 -0
- package/skills/global_config/github-actions-validator/references/action_versions.md +122 -0
- package/skills/global_config/github-actions-validator/references/actionlint_usage.md +343 -0
- package/skills/global_config/github-actions-validator/references/common_errors.md +512 -0
- package/skills/global_config/github-actions-validator/references/modern_features.md +384 -0
- package/skills/global_config/github-actions-validator/references/runners.md +317 -0
- package/skills/global_config/github-actions-validator/scripts/install_tools.sh +113 -0
- package/skills/global_config/github-actions-validator/scripts/validate_workflow.sh +910 -0
- package/skills/global_config/github-actions-validator/tests/test_validate_workflow.sh +237 -0
- package/skills/global_config/makefile-generator/SKILL.md +614 -0
- package/skills/global_config/makefile-generator/assets/templates/.gitkeep +1 -0
- package/skills/global_config/makefile-generator/docs/makefile-structure.md +530 -0
- package/skills/global_config/makefile-generator/docs/optimization-guide.md +784 -0
- package/skills/global_config/makefile-generator/docs/patterns-guide.md +642 -0
- package/skills/global_config/makefile-generator/docs/security-guide.md +361 -0
- package/skills/global_config/makefile-generator/docs/targets-guide.md +642 -0
- package/skills/global_config/makefile-generator/docs/variables-guide.md +596 -0
- package/skills/global_config/makefile-generator/scripts/add_standard_targets.sh +539 -0
- package/skills/global_config/makefile-generator/scripts/generate_makefile_template.sh +690 -0
- package/skills/global_config/makefile-generator/test/test_helper_scripts.sh +190 -0
- package/skills/global_config/makefile-validator/SKILL.md +244 -0
- package/skills/global_config/makefile-validator/docs/bake-tool.md +1000 -0
- package/skills/global_config/makefile-validator/docs/best-practices.md +858 -0
- package/skills/global_config/makefile-validator/docs/common-mistakes.md +944 -0
- package/skills/global_config/makefile-validator/examples/bad-makefile.mk +77 -0
- package/skills/global_config/makefile-validator/examples/good-makefile.mk +103 -0
- package/skills/global_config/makefile-validator/scripts/test_validate.sh +382 -0
- package/skills/global_config/makefile-validator/scripts/validate_makefile.sh +712 -0
- package/skills/global_config/{MCP_Manage → mcp-manage}/SKILL.md +3 -3
- package/skills/global_config/requesting-code-review/SKILL.md +98 -0
- package/skills/global_config/requesting-code-review/code-reviewer.md +198 -0
- package/skills/global_config/using-git-worktrees/SKILL.md +170 -0
- package/skills/global_config/verification-before-completion/SKILL.md +123 -0
- package/skills/global_config/writing-plans/SKILL.md +126 -46
- package/skills/global_config/writing-plans-legacy/SKILL.md +139 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AOS CLI -- the AOS dispatcher contract on top of the pi coding agent.
|
|
3
|
+
//
|
|
4
|
+
// What this is: a launcher. Everything it provides already exists --
|
|
5
|
+
// * the AOS skills live in ~/.agents/skills, which the `aos` installer
|
|
6
|
+
// writes and which pi discovers natively (Agent Skills spec);
|
|
7
|
+
// * the 7-node dispatcher graph lives in ~/.agents/AGENTS.md.
|
|
8
|
+
//
|
|
9
|
+
// What this is not: a second copy of the AOS payload. It reads ~/.agents at
|
|
10
|
+
// runtime and never imports @hybridlabor-api/aos, so AOS CLI and the installer
|
|
11
|
+
// version and update independently. That is why pi updates are cheap here and
|
|
12
|
+
// AOS updates are cheap there.
|
|
13
|
+
//
|
|
14
|
+
// aos-cli open the chat in the current directory
|
|
15
|
+
// aos-cli "fix the failing go build" same, with a first prompt
|
|
16
|
+
// aos-cli --continue resume the last session for this project
|
|
17
|
+
// any other flag is handed to pi unchanged
|
|
18
|
+
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const agents = path.join(os.homedir(), '.agents');
|
|
27
|
+
|
|
28
|
+
const args = [
|
|
29
|
+
'--theme', path.join(root, 'themes', 'aos.json'),
|
|
30
|
+
'--use-theme', 'aos',
|
|
31
|
+
'--extension', path.join(root, 'extensions', 'aos.ts'),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// pi discovers AGENTS.md from the working directory and its ancestors only, and
|
|
35
|
+
// ~/.agents/AGENTS.md is neither. Without this flag the dispatcher graph -- the
|
|
36
|
+
// one thing a bare pi genuinely cannot see -- is silently missing.
|
|
37
|
+
const dispatch = path.join(agents, 'AGENTS.md');
|
|
38
|
+
if (existsSync(dispatch)) {
|
|
39
|
+
args.push('--append-system-prompt', dispatch);
|
|
40
|
+
} else {
|
|
41
|
+
console.error('AOS CLI: ~/.agents not found -- run `npx @hybridlabor-api/aos` first.');
|
|
42
|
+
console.error(' Starting without the AOS skills and the dispatcher graph.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// pi advertises every discovered skill by name and description in EVERY system
|
|
46
|
+
// prompt. Measured on this machine: 210 skills, ~11.150 tokens per request, of
|
|
47
|
+
// which the four firecrawl entries alone are ~2.250. That is a permanent tax on
|
|
48
|
+
// every turn to make one skill discoverable.
|
|
49
|
+
//
|
|
50
|
+
// core-skills.json narrows it to the handful worth having always. pi does the
|
|
51
|
+
// work natively: --no-skills stops the discovery walk, and each --skill is added
|
|
52
|
+
// back explicitly. --skill alone would not do it -- it appends to discovery
|
|
53
|
+
// rather than replacing it.
|
|
54
|
+
//
|
|
55
|
+
// The tradeoff is that a skill outside this list is not merely unadvertised but
|
|
56
|
+
// unreachable, not even via /skill:name. `aos-store` is how one gets added.
|
|
57
|
+
const core = coreSkills();
|
|
58
|
+
if (core.length) {
|
|
59
|
+
args.push('--no-skills');
|
|
60
|
+
for (const name of core) args.push('--skill', path.join(agents, 'skills', name));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The core skill list, skipping names that are not installed.
|
|
65
|
+
*
|
|
66
|
+
* A missing entry is not fatal: the point of the list is to cut the prompt
|
|
67
|
+
* tax, and a skill that was pruned or renamed upstream should quietly reduce
|
|
68
|
+
* the set rather than stop pi from starting.
|
|
69
|
+
*/
|
|
70
|
+
function coreSkills() {
|
|
71
|
+
const file = path.join(root, 'core-skills.json');
|
|
72
|
+
let names;
|
|
73
|
+
try {
|
|
74
|
+
names = JSON.parse(readFileSync(file, 'utf8'));
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`AOS CLI: core-skills.json unreadable (${err.message}) -- loading all skills.`);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(names)) {
|
|
80
|
+
console.error('AOS CLI: core-skills.json is not a list -- loading all skills.');
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const found = names.filter((n) => typeof n === 'string' && existsSync(path.join(agents, 'skills', n)));
|
|
84
|
+
const absent = names.length - found.length;
|
|
85
|
+
if (absent) console.error(`AOS CLI: ${absent} core skill(s) not installed, skipped.`);
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Find pi's CLI entry by walking up to the package root that declares it.
|
|
90
|
+
// Neither require.resolve nor a fixed depth works: pi's `exports` map exports
|
|
91
|
+
// only its library entry, so `@earendil-works/pi-coding-agent/package.json`
|
|
92
|
+
// and `/dist/bundle/cli.js` both throw ERR_PACKAGE_PATH_NOT_EXPORTED, and
|
|
93
|
+
// npm leaves no node_modules/.bin/pi behind for a transitive dependency.
|
|
94
|
+
// The resolved import specifier points inside the package, so from there the
|
|
95
|
+
// root is at most a couple of levels up.
|
|
96
|
+
function findPiEntry(startDir) {
|
|
97
|
+
let dir = startDir;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const manifest = path.join(dir, 'package.json');
|
|
100
|
+
if (existsSync(manifest)) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
103
|
+
if (parsed.bin && parsed.bin.pi) return path.resolve(dir, parsed.bin.pi);
|
|
104
|
+
} catch {
|
|
105
|
+
// unreadable or malformed -- keep walking, a parent's manifest may still be pi's
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const parent = path.dirname(dir);
|
|
109
|
+
if (parent === dir) return null;
|
|
110
|
+
dir = parent;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function piInvocation() {
|
|
115
|
+
try {
|
|
116
|
+
const entry = findPiEntry(
|
|
117
|
+
path.dirname(fileURLToPath(import.meta.resolve('@earendil-works/pi-coding-agent')))
|
|
118
|
+
);
|
|
119
|
+
if (entry) return { command: process.execPath, prefix: [entry] };
|
|
120
|
+
} catch {
|
|
121
|
+
// pi is not installed next to us -- fall through to PATH
|
|
122
|
+
}
|
|
123
|
+
return { command: 'pi', prefix: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { command, prefix } = piInvocation();
|
|
127
|
+
const child = spawn(command, [...prefix, ...args, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
128
|
+
|
|
129
|
+
child.on('error', (err) => {
|
|
130
|
+
console.error(`AOS CLI: cannot start pi (${err.code ?? err.message}).`);
|
|
131
|
+
console.error(' Install it with: npm i -g @earendil-works/pi-coding-agent');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
134
|
+
child.on('exit', (code, signal) => process.exit(signal ? 1 : code ?? 0));
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AOS CLI commands -- `/aos` and `/aos-status`.
|
|
3
|
+
*
|
|
4
|
+
* Read-only by default. The AOS installer owns the terminal: it animates, it
|
|
5
|
+
* prompts, and pi's own docs say an extension must not create a second
|
|
6
|
+
* terminal renderer. So anything that needs a real TTY (install, update,
|
|
7
|
+
* uninstall) is reported as a command to run outside the session rather than
|
|
8
|
+
* spawned here -- a nested installer inside pi's TUI corrupts both.
|
|
9
|
+
*
|
|
10
|
+
* `aos-doctor --json` is the only thing this executes, and it is read-only.
|
|
11
|
+
* The shape it returns is stable and is asserted by tests/aos-doctor.test.mjs:
|
|
12
|
+
* { ok, platform, arch, results: [{ area, name, ok, detail, warningOnly }] }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { connect } from "node:net";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
// DynamicBorder is re-exported by the agent package, not by pi-tui -- the
|
|
19
|
+
// examples/extensions/preset.ts reference splits them the same way.
|
|
20
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { Container, SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
|
22
|
+
|
|
23
|
+
const DASHBOARD_PORT = 7900;
|
|
24
|
+
const DOCTOR_TIMEOUT_MS = 20_000;
|
|
25
|
+
const AUTH_TIMEOUT_MS = 8_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Providers to check for credentials, in priority order.
|
|
29
|
+
*
|
|
30
|
+
* A second provider slots in here and nowhere else -- the check, the status
|
|
31
|
+
* row and the startup notice all read this list. pi owns provider auth, not the
|
|
32
|
+
* AOS installer: it already has a `/login` command, and the AOS installer has no
|
|
33
|
+
* business asking for an Anthropic key it will never use itself. AOS CLI only
|
|
34
|
+
* has to notice when nothing is configured and point at `/login`.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately short. Reporting "no configured provider (checked: ...)" with an
|
|
37
|
+
* honest list beats claiming a global state this process cannot actually see.
|
|
38
|
+
*/
|
|
39
|
+
const PROVIDERS = ["anthropic", "google"] as const;
|
|
40
|
+
|
|
41
|
+
interface DoctorResult {
|
|
42
|
+
area: string;
|
|
43
|
+
name: string;
|
|
44
|
+
ok: boolean;
|
|
45
|
+
detail: string;
|
|
46
|
+
warningOnly: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface DoctorReport {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
results: DoctorResult[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run aos-doctor and return its report whatever the exit code.
|
|
55
|
+
*
|
|
56
|
+
* aos-doctor exits 1 whenever any single check is not ok -- that is the normal
|
|
57
|
+
* "something needs attention" answer, not a failure to run. Rejecting on a
|
|
58
|
+
* non-zero exit threw away a valid 27-check report and rendered as
|
|
59
|
+
* "aos-doctor could not be run", which is the worst possible reading: it
|
|
60
|
+
* reports the absence of a check as if the check had passed. The report's own
|
|
61
|
+
* `ok` field is the verdict; the exit code carries no extra information.
|
|
62
|
+
*/
|
|
63
|
+
function runDoctor(): Promise<DoctorReport> {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
// shell on Windows only: the global shims are .cmd there, and spawn
|
|
66
|
+
// does not resolve those without it.
|
|
67
|
+
const child = spawn("aos-doctor", ["--json"], {
|
|
68
|
+
timeout: DOCTOR_TIMEOUT_MS,
|
|
69
|
+
shell: process.platform === "win32",
|
|
70
|
+
});
|
|
71
|
+
let out = "";
|
|
72
|
+
let err = "";
|
|
73
|
+
child.stdout?.on("data", (c) => {
|
|
74
|
+
out += c;
|
|
75
|
+
});
|
|
76
|
+
child.stderr?.on("data", (c) => {
|
|
77
|
+
err += c;
|
|
78
|
+
});
|
|
79
|
+
child.once("error", reject);
|
|
80
|
+
child.once("close", (code) => {
|
|
81
|
+
try {
|
|
82
|
+
resolve(JSON.parse(out) as DoctorReport);
|
|
83
|
+
} catch {
|
|
84
|
+
reject(new Error(err.trim() || `aos-doctor produced no JSON (exit ${code})`));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ask pi whether one provider has usable credentials.
|
|
92
|
+
*
|
|
93
|
+
* `pi auth check --provider <name> --json` is a documented CLI surface and
|
|
94
|
+
* exits 0 for every verdict, including not_ready, so the JSON is the answer and
|
|
95
|
+
* the exit code carries nothing. `pi --list-models` was the tempting shortcut
|
|
96
|
+
* and is wrong: it lists every provider pi knows about whether or not a key is
|
|
97
|
+
* present, so a model in the list says nothing about being able to use it.
|
|
98
|
+
*
|
|
99
|
+
* A provider this check cannot reach at all is "unknown", never "not ready" --
|
|
100
|
+
* those are different states and reporting one as the other is how a health
|
|
101
|
+
* check ends up lying.
|
|
102
|
+
*/
|
|
103
|
+
function checkProvider(provider: string): Promise<"ready" | "not_ready" | "unknown"> {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
// argv[1] is pi's own bundle, because pi is the process running this
|
|
106
|
+
// extension. PATH is the fallback for a launch that re-execs a bare `pi`.
|
|
107
|
+
const entry = process.argv[1];
|
|
108
|
+
const [command, prefix] =
|
|
109
|
+
entry && /pi/i.test(entry) ? [process.execPath, [entry]] : ["pi", []];
|
|
110
|
+
|
|
111
|
+
const child = spawn(command, [...prefix, "auth", "check", "--provider", provider, "--json"], {
|
|
112
|
+
timeout: AUTH_TIMEOUT_MS,
|
|
113
|
+
shell: process.platform === "win32",
|
|
114
|
+
});
|
|
115
|
+
let out = "";
|
|
116
|
+
child.stdout?.on("data", (c) => {
|
|
117
|
+
out += c;
|
|
118
|
+
});
|
|
119
|
+
child.once("error", () => resolve("unknown"));
|
|
120
|
+
child.once("close", () => {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(out) as { status?: string };
|
|
123
|
+
resolve(parsed.status === "ready" ? "ready" : "not_ready");
|
|
124
|
+
} catch {
|
|
125
|
+
resolve("unknown");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One row per provider, ready ones first, for the status overlay. */
|
|
132
|
+
async function providerStatus(): Promise<{ provider: string; state: string }[]> {
|
|
133
|
+
return Promise.all(
|
|
134
|
+
PROVIDERS.map(async (provider) => ({ provider, state: await checkProvider(provider) })),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function portOpen(port: number, timeoutMs = 500): Promise<boolean> {
|
|
139
|
+
return new Promise((resolve) => {
|
|
140
|
+
const socket = connect({ port, host: "127.0.0.1" });
|
|
141
|
+
const done = (up: boolean) => {
|
|
142
|
+
socket.destroy();
|
|
143
|
+
resolve(up);
|
|
144
|
+
};
|
|
145
|
+
socket.setTimeout(timeoutMs);
|
|
146
|
+
socket.once("connect", () => done(true));
|
|
147
|
+
socket.once("timeout", () => done(false));
|
|
148
|
+
socket.once("error", () => done(false));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function showStatus(ctx: ExtensionContext): Promise<void> {
|
|
153
|
+
let report: DoctorReport | null = null;
|
|
154
|
+
let failure: string | null = null;
|
|
155
|
+
try {
|
|
156
|
+
report = await runDoctor();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
159
|
+
}
|
|
160
|
+
const dashboardUp = await portOpen(DASHBOARD_PORT);
|
|
161
|
+
const providers = await providerStatus();
|
|
162
|
+
const anyReady = providers.some((p) => p.state === "ready");
|
|
163
|
+
|
|
164
|
+
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
165
|
+
const container = new Container();
|
|
166
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
167
|
+
|
|
168
|
+
const mark = (ok: boolean) => theme.fg(ok ? "success" : "error", ok ? "●" : "○");
|
|
169
|
+
|
|
170
|
+
if (failure) {
|
|
171
|
+
container.addChild(new Text(theme.fg("error", "aos-doctor could not be run")));
|
|
172
|
+
container.addChild(new Text(theme.fg("muted", failure)));
|
|
173
|
+
container.addChild(new Text(theme.fg("dim", "It is installed by `npx @hybridlabor-api/aos`.")));
|
|
174
|
+
} else if (report) {
|
|
175
|
+
const failing = report.results.filter((r) => !r.ok);
|
|
176
|
+
container.addChild(
|
|
177
|
+
new Text(
|
|
178
|
+
theme.bold(
|
|
179
|
+
`AOS ${failing.length === 0 ? theme.fg("success", "healthy") : theme.fg("error", `${failing.length} failing check(s)`)}`,
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
container.addChild(new Text(""));
|
|
184
|
+
for (const r of report.results) {
|
|
185
|
+
container.addChild(new Text(`${mark(r.ok)} ${r.name}${r.ok ? "" : theme.fg("error", " ← needs attention")}`));
|
|
186
|
+
if (r.detail) container.addChild(new Text(theme.fg("muted", ` ${r.detail}`)));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
container.addChild(new Text(""));
|
|
191
|
+
container.addChild(
|
|
192
|
+
new Text(
|
|
193
|
+
`${mark(dashboardUp)} Dashboard :${DASHBOARD_PORT} ${
|
|
194
|
+
dashboardUp ? theme.fg("success", "running") : theme.fg("dim", "stopped -- run `npx aos-dashboard`")
|
|
195
|
+
}`,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
container.addChild(
|
|
199
|
+
new Text(
|
|
200
|
+
`${mark(anyReady)} Model ${
|
|
201
|
+
anyReady
|
|
202
|
+
? theme.fg("success", providers.filter((p) => p.state === "ready").map((p) => p.provider).join(", "))
|
|
203
|
+
: theme.fg("error", "no provider configured -- run /login in pi")
|
|
204
|
+
}`,
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
container.addChild(new Text(""));
|
|
208
|
+
container.addChild(new Text(theme.fg("dim", "esc or enter to close")));
|
|
209
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
render(width: number) {
|
|
213
|
+
return container.render(width);
|
|
214
|
+
},
|
|
215
|
+
invalidate() {
|
|
216
|
+
container.invalidate();
|
|
217
|
+
},
|
|
218
|
+
handleInput(data: string) {
|
|
219
|
+
if (data === "" || data === "\r" || data === "\n" || data === "") done();
|
|
220
|
+
tui.requestRender();
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default function aosCommands(pi: ExtensionAPI) {
|
|
227
|
+
// Fire and forget on session_start: never block startup on it, and never
|
|
228
|
+
// repeat. A user without credentials can still read skills, run
|
|
229
|
+
// /aos-status and browse -- only sending a message needs a model, so this is
|
|
230
|
+
// a notice, not a gate.
|
|
231
|
+
//
|
|
232
|
+
// It says "at startup" on purpose. This handler runs once, so a /login
|
|
233
|
+
// completed inside the session leaves the warning standing next to a working
|
|
234
|
+
// model -- and a notice that is visible after you acted on it is worse than
|
|
235
|
+
// none, because it reads as a current fact. pi has no event this extension
|
|
236
|
+
// can rely on for "a login just landed", so rather than invent a poll or
|
|
237
|
+
// guess at an API, the wording states when it was true and /aos-status is
|
|
238
|
+
// the live reading. Measured: `pi auth check` returns ready/oauth once the
|
|
239
|
+
// browser flow completes, so that command and nothing else is the truth.
|
|
240
|
+
let warnedNoProvider = false;
|
|
241
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
242
|
+
if (warnedNoProvider) return;
|
|
243
|
+
const providers = await providerStatus();
|
|
244
|
+
if (providers.some((p) => p.state === "ready")) return;
|
|
245
|
+
warnedNoProvider = true;
|
|
246
|
+
const unknown = providers.filter((p) => p.state === "unknown").map((p) => p.provider);
|
|
247
|
+
ctx.ui.notify(
|
|
248
|
+
`No model provider was configured at startup (checked: ${providers.map((p) => p.provider).join(", ")}). ` +
|
|
249
|
+
`Run /login to add one -- or /aos-status for the current state.`,
|
|
250
|
+
"warning",
|
|
251
|
+
);
|
|
252
|
+
if (unknown.length) {
|
|
253
|
+
ctx.ui.notify(`Could not reach pi's auth check for: ${unknown.join(", ")} -- treated as unverified, not as unconfigured.`, "info");
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
pi.registerCommand("aos-status", {
|
|
258
|
+
description: "AOS health, failing checks and daemon status",
|
|
259
|
+
handler: async (_args, ctx) => {
|
|
260
|
+
await showStatus(ctx);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.registerCommand("aos", {
|
|
265
|
+
description: "AOS menu: status, dashboard, or the command for a terminal action",
|
|
266
|
+
handler: async (_args, ctx) => {
|
|
267
|
+
const items: SelectItem[] = [
|
|
268
|
+
{ value: "status", label: "Status", description: "run aos-doctor and show failing checks" },
|
|
269
|
+
{ value: "update", label: "Skills & MCPs aktualisieren", description: "runs outside this session" },
|
|
270
|
+
{ value: "install", label: "AOS CLI nachinstallieren", description: "runs outside this session" },
|
|
271
|
+
{ value: "uninstall", label: "AOS deinstallieren", description: "runs outside this session" },
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const choice = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
275
|
+
const container = new Container();
|
|
276
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
277
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("AOS"))));
|
|
278
|
+
|
|
279
|
+
const list = new SelectList(items, items.length, {
|
|
280
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
281
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
282
|
+
description: (t) => theme.fg("muted", t),
|
|
283
|
+
});
|
|
284
|
+
list.onSelect = (item) => done(item.value);
|
|
285
|
+
list.onCancel = () => done(null);
|
|
286
|
+
container.addChild(list);
|
|
287
|
+
|
|
288
|
+
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")));
|
|
289
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
render(width: number) {
|
|
293
|
+
return container.render(width);
|
|
294
|
+
},
|
|
295
|
+
invalidate() {
|
|
296
|
+
container.invalidate();
|
|
297
|
+
},
|
|
298
|
+
handleInput(data: string) {
|
|
299
|
+
list.handleInput(data);
|
|
300
|
+
tui.requestRender();
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (choice === "status") {
|
|
306
|
+
await showStatus(ctx);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!choice) return;
|
|
310
|
+
|
|
311
|
+
// The installer animates and prompts; running it with inherited stdio
|
|
312
|
+
// inside pi's TUI corrupts both renderers. Report the command instead.
|
|
313
|
+
const commands: Record<string, string> = {
|
|
314
|
+
update: "aos",
|
|
315
|
+
install: "aos",
|
|
316
|
+
uninstall: "npx @hybridlabor-api/aos-uninstall",
|
|
317
|
+
};
|
|
318
|
+
ctx.ui.notify(`Run this in a separate terminal: ${commands[choice]}`, "info");
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|